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