blob: 2d1a21fd56ed78231f7e55812b3cd7912e677520 [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.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.stream.Collectors;
/** Factory methods for producing {@link Crash}-type {@link FailureDetail} messages. */
public class CrashFailureDetails {
/**
* 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 CrashFailureDetails() {}
public static DetailedExitCode detailedExitCodeForThrowable(Throwable throwable) {
return DetailedExitCode.of(forThrowable(throwable));
}
/**
* Returns a {@link Crash}-type {@link FailureDetail} with {@link Crash.Code#CRASH_UNKNOWN}, with
* its cause chain filled out.
*/
public static FailureDetail forThrowable(Throwable throwable) {
Crash.Builder crashBuilder =
Crash.newBuilder()
.setCode(
(getRootCauseToleratingCycles(throwable) instanceof OutOfMemoryError)
? Crash.Code.CRASH_OOM
: 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;
}
}