From 671e7e2f0004c0ec94b4d9f199f1b18c8e71c179 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 15 Feb 2018 09:48:52 -0500 Subject: [PATCH] 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 --- .../transport/netty4/Netty4Utils.java | 43 +-------- .../transport/netty4/Netty4UtilsTests.java | 58 ------------- .../org/elasticsearch/ExceptionsHelper.java | 40 +++++++++ .../elasticsearch/ExceptionsHelperTests.java | 87 +++++++++++++++++++ 4 files changed, 129 insertions(+), 99 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java index 8d1c9d61a0b..9470424b381 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java @@ -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 maybeError = maybeError(cause); + final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class); + final Optional 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 maybeError(final Throwable cause) { - // early terminate if the cause is already an error - if (cause instanceof Error) { - return Optional.of((Error) cause); - } - - final Queue 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(); - } - } diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4UtilsTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4UtilsTests.java index 43be6f0efdd..8372a8540b8 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4UtilsTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4UtilsTests.java @@ -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 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); diff --git a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java index 902bcee63fb..05ac4d942b3 100644 --- a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java +++ b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java @@ -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 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 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 diff --git a/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java b/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java new file mode 100644 index 00000000000..011f5b380ec --- /dev/null +++ b/server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java @@ -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 maybeError = maybeError(cause, logger); + assertTrue(maybeError.isPresent()); + assertThat(maybeError.get(), equalTo(error)); + } + +}