| // 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; |
| } |
| } |