blob: 5857b72d7195aee4b959bbc57979adeb90953f36 [file] [log] [blame]
// Copyright 2020 The Bazel Authors. All rights reserved.
//
// Licensed 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 com.google.devtools.build.lib.util;
import com.google.common.base.Ascii;
import com.google.common.base.Throwables;
import com.google.common.collect.Sets;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Crash;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.ThrowableOrBuilder;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
/** Factory methods for producing {@link Crash}-type {@link FailureDetail} messages. */
public class CrashFailureDetails {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
/**
* Max message length in {@link FailureDetails.Throwable} submessage, anything beyond this is
* truncated.
*/
private static final int MAX_THROWABLE_MESSAGE_LENGTH = 2000;
/**
* At most this many {@link FailureDetails.Throwable} messages will be specified by a {@link
* Crash} submessage.
*/
private static final int MAX_CAUSE_CHAIN_SIZE = 5;
/**
* At most this many stack trace element strings will be specified by a {@link
* FailureDetails.Throwable} submessage.
*/
private static final int MAX_STACK_TRACE_SIZE = 1000;
private static BooleanSupplier oomDetector = () -> false;
private CrashFailureDetails() {}
/** Registers a predicate to use for more aggressive {@link OutOfMemoryError} detection. */
public static void setOomDetector(BooleanSupplier oomDetector) {
CrashFailureDetails.oomDetector = oomDetector;
}
/** Returns whether an {@link OutOfMemoryError} was detected. */
public static boolean oomDetected() {
return oomDetector.getAsBoolean();
}
public static DetailedExitCode detailedExitCodeForThrowable(Throwable throwable) {
return DetailedExitCode.of(forThrowable(throwable));
}
/** Returns a {@link Crash}-type {@link FailureDetail} with its cause chain filled out. */
public static FailureDetail forThrowable(Throwable throwable) {
Crash.Builder crashBuilder = Crash.newBuilder();
if (getRootCauseToleratingCycles(throwable) instanceof OutOfMemoryError) {
crashBuilder.setCode(Crash.Code.CRASH_OOM);
} else if (oomDetected()) {
logger.atWarning().log("Classifying non-OOM crash as OOM");
crashBuilder.setCode(Crash.Code.CRASH_OOM).setOomDetectorOverride(true);
} else {
crashBuilder.setCode(Crash.Code.CRASH_UNKNOWN);
}
addCause(crashBuilder, throwable, Sets.newIdentityHashSet());
return FailureDetail.newBuilder()
.setMessage("Crashed: " + joinSummarizedCauses(crashBuilder))
.setCrash(crashBuilder)
.build();
}
private static String joinSummarizedCauses(Crash.Builder crashBuilder) {
return crashBuilder.getCausesOrBuilderList().stream()
.map(CrashFailureDetails::summarizeCause)
.collect(Collectors.joining(", "));
}
private static String summarizeCause(ThrowableOrBuilder throwableOrBuilder) {
return String.format(
"(%s) %s", throwableOrBuilder.getThrowableClass(), throwableOrBuilder.getMessage());
}
private static void addCause(
Crash.Builder crashBuilder, Throwable throwable, Set<Object> addedThrowables) {
addedThrowables.add(throwable);
crashBuilder.addCauses(getThrowable(throwable));
Throwable cause = throwable.getCause();
if (cause == null
|| addedThrowables.contains(cause)
|| crashBuilder.getCausesOrBuilderList().size() >= MAX_CAUSE_CHAIN_SIZE) {
return;
}
addCause(crashBuilder, cause, addedThrowables);
}
private static FailureDetails.Throwable getThrowable(Throwable throwable) {
String throwableMessage =
Ascii.truncate(
throwable.getMessage() != null ? throwable.getMessage() : "",
MAX_THROWABLE_MESSAGE_LENGTH,
"[truncated]");
FailureDetails.Throwable.Builder throwableBuilder =
FailureDetails.Throwable.newBuilder()
.setMessage(throwableMessage)
.setThrowableClass(throwable.getClass().getName());
StackTraceElement[] stackTrace = throwable.getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if (throwableBuilder.getStackTraceList().size() >= MAX_STACK_TRACE_SIZE) {
break;
}
throwableBuilder.addStackTrace(stackTraceElement.toString());
}
return throwableBuilder.build();
}
/**
* Returns the innermost cause of {@code throwable}. The first throwable in a chain provides
* context from when the error or exception was initially detected. Example usage:
*
* <pre>
* assertEquals("Unable to assign a customer id", Throwables.getRootCause(e).getMessage());
* </pre>
*
* Cloned from {@link Throwables#getRootCause} with a modification to return an arbitrary element
* of the cycle rather than throw if there is a causal cycle.
*/
private static Throwable getRootCauseToleratingCycles(Throwable throwable) {
// Keep a second pointer that slowly walks the causal chain. If the fast pointer ever catches
// the slower pointer, then there's a loop.
Throwable slowPointer = throwable;
boolean advanceSlowPointer = false;
Throwable cause;
while ((cause = throwable.getCause()) != null) {
throwable = cause;
if (throwable == slowPointer) {
// There's a cycle: choose an arbitrary element in that cycle.
return throwable;
}
if (advanceSlowPointer) {
slowPointer = slowPointer.getCause();
}
advanceSlowPointer = !advanceSlowPointer; // only advance every other iteration
}
return throwable;
}
}