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
This commit is contained in:
Jason Tedor 2018-02-15 09:48:52 -05:00 committed by GitHub
parent 3e07c6ff54
commit 671e7e2f00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 99 deletions

View File

@ -38,12 +38,9 @@ import org.elasticsearch.common.logging.ESLoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
public class Netty4Utils {
@ -171,7 +168,8 @@ public class Netty4Utils {
* @param cause the throwable to test
*/
public static void maybeDie(final Throwable cause) {
final Optional<Error> maybeError = maybeError(cause);
final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class);
final Optional<Error> maybeError = ExceptionsHelper.maybeError(cause, logger);
if (maybeError.isPresent()) {
/*
* Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, Netty wraps too many
@ -182,7 +180,6 @@ public class Netty4Utils {
try {
// try to log the current stack trace
final String formatted = ExceptionsHelper.formatStackTrace(Thread.currentThread().getStackTrace());
final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class);
logger.error("fatal error on the network layer\n{}", formatted);
} finally {
new Thread(
@ -194,40 +191,4 @@ public class Netty4Utils {
}
}
static final int MAX_ITERATIONS = 1024;
/**
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
*
* @param cause the root throwable
*
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
*/
static Optional<Error> maybeError(final Throwable cause) {
// early terminate if the cause is already an error
if (cause instanceof Error) {
return Optional.of((Error) cause);
}
final Queue<Throwable> queue = new LinkedList<>();
queue.add(cause);
int iterations = 0;
while (!queue.isEmpty()) {
iterations++;
if (iterations > MAX_ITERATIONS) {
ESLoggerFactory.getLogger(Netty4Utils.class).warn("giving up looking for fatal errors on the network layer", cause);
break;
}
final Throwable current = queue.remove();
if (current instanceof Error) {
return Optional.of((Error) current);
}
Collections.addAll(queue, current.getSuppressed());
if (current.getCause() != null) {
queue.add(current.getCause());
}
}
return Optional.empty();
}
}

View File

@ -22,7 +22,6 @@ package org.elasticsearch.transport.netty4;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.DecoderException;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.bytes.AbstractBytesReferenceTestCase;
import org.elasticsearch.common.bytes.BytesArray;
@ -33,9 +32,6 @@ import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.Optional;
import static org.hamcrest.CoreMatchers.equalTo;
public class Netty4UtilsTests extends ESTestCase {
@ -79,60 +75,6 @@ public class Netty4UtilsTests extends ESTestCase {
assertArrayEquals(BytesReference.toBytes(ref), BytesReference.toBytes(bytesReference));
}
public void testMaybeError() {
final Error outOfMemoryError = new OutOfMemoryError();
assertError(outOfMemoryError, outOfMemoryError);
final DecoderException decoderException = new DecoderException(outOfMemoryError);
assertError(decoderException, outOfMemoryError);
final Exception e = new Exception();
e.addSuppressed(decoderException);
assertError(e, outOfMemoryError);
final int depth = randomIntBetween(1, 16);
Throwable cause = new Exception();
boolean fatal = false;
Error error = null;
for (int i = 0; i < depth; i++) {
final int length = randomIntBetween(1, 4);
for (int j = 0; j < length; j++) {
if (!fatal && rarely()) {
error = new Error();
cause.addSuppressed(error);
fatal = true;
} else {
cause.addSuppressed(new Exception());
}
}
if (!fatal && rarely()) {
cause = error = new Error(cause);
fatal = true;
} else {
cause = new Exception(cause);
}
}
if (fatal) {
assertError(cause, error);
} else {
assertFalse(Netty4Utils.maybeError(cause).isPresent());
}
assertFalse(Netty4Utils.maybeError(new Exception(new DecoderException())).isPresent());
Throwable chain = outOfMemoryError;
for (int i = 0; i < Netty4Utils.MAX_ITERATIONS; i++) {
chain = new Exception(chain);
}
assertFalse(Netty4Utils.maybeError(chain).isPresent());
}
private void assertError(final Throwable cause, final Error error) {
final Optional<Error> maybeError = Netty4Utils.maybeError(cause);
assertTrue(maybeError.isPresent());
assertThat(maybeError.get(), equalTo(error));
}
private BytesReference getRandomizedBytesReference(int length) throws IOException {
// we know bytes stream output always creates a paged bytes reference, we use it to create randomized content
ReleasableBytesStreamOutput out = new ReleasableBytesStreamOutput(length, bigarrays);

View File

@ -34,8 +34,12 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;
@ -128,6 +132,42 @@ public final class ExceptionsHelper {
return Arrays.stream(stackTrace).skip(1).map(e -> "\tat " + e).collect(Collectors.joining("\n"));
}
static final int MAX_ITERATIONS = 1024;
/**
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
*
* @param cause the root throwable
*
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
*/
public static Optional<Error> maybeError(final Throwable cause, final Logger logger) {
// early terminate if the cause is already an error
if (cause instanceof Error) {
return Optional.of((Error) cause);
}
final Queue<Throwable> queue = new LinkedList<>();
queue.add(cause);
int iterations = 0;
while (!queue.isEmpty()) {
iterations++;
if (iterations > MAX_ITERATIONS) {
logger.warn("giving up looking for fatal errors", cause);
break;
}
final Throwable current = queue.remove();
if (current instanceof Error) {
return Optional.of((Error) current);
}
Collections.addAll(queue, current.getSuppressed());
if (current.getCause() != null) {
queue.add(current.getCause());
}
}
return Optional.empty();
}
/**
* Rethrows the first exception in the list and adds all remaining to the suppressed list.
* If the given list is empty no exception is thrown

View File

@ -0,0 +1,87 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch;
import org.apache.commons.codec.DecoderException;
import org.elasticsearch.test.ESTestCase;
import java.util.Optional;
import static org.elasticsearch.ExceptionsHelper.MAX_ITERATIONS;
import static org.elasticsearch.ExceptionsHelper.maybeError;
import static org.hamcrest.CoreMatchers.equalTo;
public class ExceptionsHelperTests extends ESTestCase {
public void testMaybeError() {
final Error outOfMemoryError = new OutOfMemoryError();
assertError(outOfMemoryError, outOfMemoryError);
final DecoderException decoderException = new DecoderException(outOfMemoryError);
assertError(decoderException, outOfMemoryError);
final Exception e = new Exception();
e.addSuppressed(decoderException);
assertError(e, outOfMemoryError);
final int depth = randomIntBetween(1, 16);
Throwable cause = new Exception();
boolean fatal = false;
Error error = null;
for (int i = 0; i < depth; i++) {
final int length = randomIntBetween(1, 4);
for (int j = 0; j < length; j++) {
if (!fatal && rarely()) {
error = new Error();
cause.addSuppressed(error);
fatal = true;
} else {
cause.addSuppressed(new Exception());
}
}
if (!fatal && rarely()) {
cause = error = new Error(cause);
fatal = true;
} else {
cause = new Exception(cause);
}
}
if (fatal) {
assertError(cause, error);
} else {
assertFalse(maybeError(cause, logger).isPresent());
}
assertFalse(maybeError(new Exception(new DecoderException()), logger).isPresent());
Throwable chain = outOfMemoryError;
for (int i = 0; i < MAX_ITERATIONS; i++) {
chain = new Exception(chain);
}
assertFalse(maybeError(chain, logger).isPresent());
}
private void assertError(final Throwable cause, final Error error) {
final Optional<Error> maybeError = maybeError(cause, logger);
assertTrue(maybeError.isPresent());
assertThat(maybeError.get(), equalTo(error));
}
}