blob: f345f717cf4e4652eb34eb75e3dbb902b8ff7ec7 [file] [log] [blame]
// Copyright 2017 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.actions;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.shell.TerminationStatus;
import com.google.devtools.build.lib.vfs.Path;
import com.google.protobuf.ByteString;
import java.io.InputStream;
import java.time.Duration;
import java.util.Locale;
import java.util.Optional;
import javax.annotation.Nullable;
/** The result of a {@link Spawn}'s execution. */
public interface SpawnResult {
int POSIX_TIMEOUT_EXIT_CODE = /*SIGNAL_BASE=*/ 128 + /*SIGALRM=*/ 14;
/** The status of the attempted Spawn execution. */
enum Status {
/** Subprocess executed successfully, and returned a zero exit code. */
SUCCESS,
/** Subprocess executed successfully, but returned a non-zero exit code. */
NON_ZERO_EXIT(true),
/** Subprocess execution timed out. */
TIMEOUT(true),
/**
* The subprocess ran out of memory. On Linux systems, the kernel may kill processes in
* low-memory situations, and this status is intended to report such a case back to Bazel.
*/
OUT_OF_MEMORY(true),
/**
* Subprocess did not execute, it's not the user's fault, and the error is not catastrophic. If
* keep_going is enabled then Bazel will try to continue the build, possibly will attempt to
* rerun the same spawn, and possibly will attempt to run other actions.
*/
EXECUTION_FAILED,
/**
* Subprocess did not execute, it's not the user's fault, and the error is catastrophic. Bazel
* will not rerun this spawn. Bazel will attempt to not run other actions (regardless of whether
* keep_going is enabled).
*/
EXECUTION_FAILED_CATASTROPHICALLY,
/**
* Subprocess did not execute, it may be the user's fault, and the error is not catastrophic.
* The user may be able to fix it. For example, a remote system may have denied the execution
* due to too many inputs or too large inputs.
*/
EXECUTION_DENIED(true),
/**
* Subprocess did not execute, it may be the user's fault, and the error is catastrophic. The
* user may be able to prevent it from reoccurring. For example, an input file's contents may
* have been modified by the user intra-build.
*/
EXECUTION_DENIED_CATASTROPHICALLY(true),
/**
* The result of the remotely executed Spawn could not be retrieved due to errors in the remote
* caching layer.
*/
REMOTE_CACHE_FAILED;
private final boolean isUserError;
Status(boolean isUserError) {
this.isUserError = isUserError;
}
Status() {
this(false);
}
public boolean isConsideredUserError() {
return isUserError;
}
}
/**
* Returns whether the spawn was actually run, regardless of the exit code. I.e., returns {@code
* true} if {@link #status} is any of {@link Status#SUCCESS}, {@link Status#NON_ZERO_EXIT}, {@link
* Status#TIMEOUT} or {@link Status#OUT_OF_MEMORY}.
*
* <p>Returns false if there were errors that prevented the spawn from being run, such as network
* errors, missing local files, errors setting up sandboxing, etc.
*/
default boolean setupSuccess() {
Status status = status();
return status == Status.SUCCESS
|| status == Status.NON_ZERO_EXIT
|| status == Status.TIMEOUT
|| status == Status.OUT_OF_MEMORY;
}
/**
* Returns true if the status was {@link Status#EXECUTION_FAILED_CATASTROPHICALLY} or {@link
* Status#EXECUTION_DENIED_CATASTROPHICALLY}.
*/
default boolean isCatastrophe() {
return status() == Status.EXECUTION_FAILED_CATASTROPHICALLY
|| status() == Status.EXECUTION_DENIED_CATASTROPHICALLY;
}
/** Returns the status of the attempted Spawn execution. */
Status status();
/**
* Returns the exit code of the subprocess if the subprocess was executed.
*
* <p>Returns zero if {@link #status} returns {@link Status#SUCCESS}.
*
* <p>Returns non-zero if {@link #status} returns {@link Status#NON_ZERO_EXIT} or {@link
* Status#OUT_OF_MEMORY}.
*
* <p>Returns 128 + 14 (corresponding to the Unix signal SIGALRM) if {@link #status} returns
* {@link Status#TIMEOUT}.
*
* <p>Otherwise, the returned value is not meaningful.
*/
// TODO(mschaller): clean up all uses of this method when {@code !this.setupSuccess()}
int exitCode();
/**
* A detailed representation of what failed if {@link #status} is not {@link Status#SUCCESS}, and
* {@code null} otherwise.
*/
@Nullable
FailureDetail failureDetail();
/**
* Returns the host name of the executor or {@code null}. This information is intended for
* debugging purposes, especially for remote execution systems. Remote caches usually do not store
* the original host name, so this is generally {@code null} for cache hits.
*/
@Nullable
String getExecutorHostName();
/**
* Returns the name of the SpawnRunner that executed the spawn. It should always be defined,
* unless isCacheHit is true, in which case the spawn was not actually run.
*/
String getRunnerName();
/**
* Returns the wall time taken by the {@link Spawn}'s execution.
*
* @return the measurement, or empty in case of execution errors or when the measurement is not
* implemented for the current platform
*/
Optional<Duration> getWallTime();
/**
* Returns the user time taken by the {@link Spawn}'s execution.
*
* @return the measurement, or empty in case of execution errors or when the measurement is not
* implemented for the current platform
*/
Optional<Duration> getUserTime();
/**
* Returns the system time taken by the {@link Spawn}'s execution.
*
* @return the measurement, or empty in case of execution errors or when the measurement is not
* implemented for the current platform
*/
Optional<Duration> getSystemTime();
/**
* Returns the number of block output operations during the {@link Spawn}'s execution.
*
* @return the measurement, or empty in case of execution errors or when the measurement is not
* implemented for the current platform
*/
Optional<Long> getNumBlockOutputOperations();
/**
* Returns the number of block input operations during the {@link Spawn}'s execution.
*
* @return the measurement, or empty in case of execution errors or when the measurement is not
* implemented for the current platform
*/
Optional<Long> getNumBlockInputOperations();
/**
* Returns the number of involuntary context switches during the {@link Spawn}'s execution.
*
* @return the measurement, or empty in case of execution errors or when the measurement is not
* implemented for the current platform
*/
Optional<Long> getNumInvoluntaryContextSwitches();
SpawnMetrics getMetrics();
/** Returns whether the spawn result was a cache hit. */
boolean isCacheHit();
/** Returns an optional custom failure message for the result. */
default String getFailureMessage() {
return "";
}
/**
* Returns a {@link Spawn}'s output in-memory, if supported and available.
*
* <p>This behavior may be triggered with {@link
* ExecutionRequirements#REMOTE_EXECUTION_INLINE_OUTPUTS}.
*/
@Nullable
default InputStream getInMemoryOutput(ActionInput output) {
return null;
}
String getDetailMessage(
String messagePrefix,
String message,
boolean verboseFailures,
boolean catastrophe,
boolean forciblyRunRemotely);
/** Returns a file path to the action metadata log. */
Optional<MetadataLog> getActionMetadataLog();
/** Whether the spawn result was obtained through remote strategy. */
boolean wasRemote();
/** Basic implementation of {@link SpawnResult}. */
@Immutable
@ThreadSafe
final class SimpleSpawnResult implements SpawnResult {
private final int exitCode;
private final Status status;
@Nullable private final FailureDetail failureDetail;
private final String executorHostName;
private final String runnerName;
private final SpawnMetrics spawnMetrics;
private final Optional<Duration> wallTime;
private final Optional<Duration> userTime;
private final Optional<Duration> systemTime;
private final Optional<Long> numBlockOutputOperations;
private final Optional<Long> numBlockInputOperations;
private final Optional<Long> numInvoluntaryContextSwitches;
private final Optional<MetadataLog> actionMetadataLog;
private final boolean cacheHit;
private final String failureMessage;
// Invariant: Either both have a value or both are null.
@Nullable private final ActionInput inMemoryOutputFile;
@Nullable private final ByteString inMemoryContents;
private final boolean remote;
SimpleSpawnResult(Builder builder) {
this.exitCode = builder.exitCode;
this.status = Preconditions.checkNotNull(builder.status);
this.failureDetail = builder.failureDetail;
this.executorHostName = builder.executorHostName;
this.runnerName = builder.runnerName;
this.spawnMetrics = builder.spawnMetrics != null
? builder.spawnMetrics
: SpawnMetrics.forLocalExecution(builder.wallTime.orElse(Duration.ZERO));
this.wallTime = builder.wallTime;
this.userTime = builder.userTime;
this.systemTime = builder.systemTime;
this.numBlockOutputOperations = builder.numBlockOutputOperations;
this.numBlockInputOperations = builder.numBlockInputOperations;
this.numInvoluntaryContextSwitches = builder.numInvoluntaryContextSwitches;
this.cacheHit = builder.cacheHit;
this.failureMessage = builder.failureMessage;
this.inMemoryOutputFile = builder.inMemoryOutputFile;
this.inMemoryContents = builder.inMemoryContents;
this.actionMetadataLog = builder.actionMetadataLog;
this.remote = builder.remote;
}
@Override
public int exitCode() {
return exitCode;
}
@Override
public Status status() {
return status;
}
@Override
@Nullable
public FailureDetail failureDetail() {
return failureDetail;
}
@Override
public String getExecutorHostName() {
return executorHostName;
}
@Override
public String getRunnerName() {
return runnerName;
}
@Override
public SpawnMetrics getMetrics() {
return spawnMetrics;
}
@Override
public Optional<Duration> getWallTime() {
return wallTime;
}
@Override
public Optional<Duration> getUserTime() {
return userTime;
}
@Override
public Optional<Duration> getSystemTime() {
return systemTime;
}
@Override
public Optional<Long> getNumBlockOutputOperations() {
return numBlockOutputOperations;
}
@Override
public Optional<Long> getNumBlockInputOperations() {
return numBlockInputOperations;
}
@Override
public Optional<Long> getNumInvoluntaryContextSwitches() {
return numInvoluntaryContextSwitches;
}
@Override
public boolean isCacheHit() {
return cacheHit;
}
@Override
public String getFailureMessage() {
return failureMessage;
}
@Override
public String getDetailMessage(
String messagePrefix,
String message,
boolean verboseFailures,
boolean catastrophe,
boolean forciblyRunRemotely) {
TerminationStatus status = new TerminationStatus(
exitCode(), status() == Status.TIMEOUT);
String reason = " (" + status.toShortString() + ")"; // e.g " (Exit 1)"
// Include the command line as error message if verbose_failures are enabled for this spawn or
// the command line didn't exit normally.
String explanation = verboseFailures || !status.exited() ? ": " + message : "";
if (!status().isConsideredUserError()) {
String errorDetail = status().name().toLowerCase(Locale.US)
.replace('_', ' ');
explanation += ". Note: Remote connection/protocol failed with: " + errorDetail;
}
if (status() == Status.TIMEOUT) {
if (getWallTime().isPresent()) {
explanation +=
String.format(
Locale.US,
" (failed due to timeout after %.2f seconds.)",
getWallTime().get().toMillis() / 1000.0);
} else {
explanation += " (failed due to timeout.)";
}
} else if (status() == Status.OUT_OF_MEMORY) {
explanation += " (Remote action was terminated due to Out of Memory.)";
}
if (status() != Status.TIMEOUT && forciblyRunRemotely) {
explanation += " Action tagged as local was forcibly run remotely and failed - it's "
+ "possible that the action simply doesn't work remotely";
}
if (!Strings.isNullOrEmpty(failureMessage)) {
explanation += " " + failureMessage;
}
return messagePrefix + " failed" + reason + explanation;
}
@Nullable
@Override
public InputStream getInMemoryOutput(ActionInput output) {
if (inMemoryOutputFile != null && inMemoryOutputFile.equals(output)) {
return inMemoryContents.newInput();
}
return null;
}
@Override
public Optional<MetadataLog> getActionMetadataLog() {
return actionMetadataLog;
}
@Override
public boolean wasRemote() {
return remote;
}
}
/** Builder class for {@link SpawnResult}. */
final class Builder {
private int exitCode;
private Status status;
private FailureDetail failureDetail;
private String executorHostName;
private String runnerName = "";
private SpawnMetrics spawnMetrics;
private Optional<Duration> wallTime = Optional.empty();
private Optional<Duration> userTime = Optional.empty();
private Optional<Duration> systemTime = Optional.empty();
private Optional<Long> numBlockOutputOperations = Optional.empty();
private Optional<Long> numBlockInputOperations = Optional.empty();
private Optional<Long> numInvoluntaryContextSwitches = Optional.empty();
private Optional<MetadataLog> actionMetadataLog = Optional.empty();
private boolean cacheHit;
private String failureMessage = "";
// Invariant: Either both have a value or both are null.
@Nullable private ActionInput inMemoryOutputFile;
@Nullable private ByteString inMemoryContents;
private boolean remote;
public SpawnResult build() {
Preconditions.checkArgument(!runnerName.isEmpty());
if (status == Status.SUCCESS) {
Preconditions.checkArgument(exitCode == 0, exitCode);
} else if (status == Status.TIMEOUT) {
Preconditions.checkArgument(exitCode == POSIX_TIMEOUT_EXIT_CODE, exitCode);
} else if (status == Status.NON_ZERO_EXIT || status == Status.OUT_OF_MEMORY) {
Preconditions.checkArgument(exitCode != 0, exitCode);
}
// TODO(mschaller): Once SimpleSpawnResult.Builder's uses have picked up FailureDetails for
// unsuccessful spawns, add a precondition that asserts failureDetail's nullity is the same
// as whether status is SUCCESS.
return new SimpleSpawnResult(this);
}
public Builder setExitCode(int exitCode) {
this.exitCode = exitCode;
return this;
}
public Builder setStatus(Status status) {
this.status = status;
return this;
}
public Builder setFailureDetail(FailureDetail failureDetail) {
this.failureDetail = failureDetail;
return this;
}
public Builder setExecutorHostname(String executorHostName) {
this.executorHostName = executorHostName;
return this;
}
public Builder setRunnerName(String runnerName) {
this.runnerName = runnerName;
return this;
}
public Builder setSpawnMetrics(SpawnMetrics spawnMetrics) {
this.spawnMetrics = spawnMetrics;
return this;
}
public Builder setWallTime(Duration wallTime) {
this.wallTime = Optional.of(wallTime);
return this;
}
public Builder setUserTime(Duration userTime) {
this.userTime = Optional.of(userTime);
return this;
}
public Builder setSystemTime(Duration systemTime) {
this.systemTime = Optional.of(systemTime);
return this;
}
public Builder setNumBlockOutputOperations(long numBlockOutputOperations) {
this.numBlockOutputOperations = Optional.of(numBlockOutputOperations);
return this;
}
public Builder setNumBlockInputOperations(long numBlockInputOperations) {
this.numBlockInputOperations = Optional.of(numBlockInputOperations);
return this;
}
public Builder setNumInvoluntaryContextSwitches(long numInvoluntaryContextSwitches) {
this.numInvoluntaryContextSwitches = Optional.of(numInvoluntaryContextSwitches);
return this;
}
public Builder setCacheHit(boolean cacheHit) {
this.cacheHit = cacheHit;
return this;
}
public Builder setFailureMessage(String failureMessage) {
this.failureMessage = failureMessage;
return this;
}
public Builder setInMemoryOutput(ActionInput outputFile, ByteString contents) {
this.inMemoryOutputFile = Preconditions.checkNotNull(outputFile);
this.inMemoryContents = Preconditions.checkNotNull(contents);
return this;
}
Builder setActionMetadataLog(MetadataLog actionMetadataLog) {
this.actionMetadataLog = Optional.of(actionMetadataLog);
return this;
}
public Builder setRemote(boolean remote) {
this.remote = remote;
return this;
}
}
/** A {@link Spawn}'s metadata name and {@link Path}. */
final class MetadataLog {
private final String name;
private final Path filePath;
public MetadataLog(String name, Path filePath) {
this.name = name;
this.filePath = filePath;
}
public String getName() {
return this.name;
}
public Path getFilePath() {
return this.filePath;
}
}
}