// Copyright 2014 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.buildtool; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.devtools.build.lib.actions.ActionCacheChecker; import com.google.devtools.build.lib.actions.ActionExecutionException; import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter; import com.google.devtools.build.lib.actions.ActionInputFileCache; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.BuildFailedException; import com.google.devtools.build.lib.actions.Executor; import com.google.devtools.build.lib.actions.MissingInputFileException; import com.google.devtools.build.lib.actions.TestExecException; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.TopLevelArtifactContext; import com.google.devtools.build.lib.buildtool.buildevent.ExecutionProgressReceiverAvailableEvent; import com.google.devtools.build.lib.events.ExtendedEventHandler; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.packages.BuildFileNotFoundException; import com.google.devtools.build.lib.rules.test.TestProvider; import com.google.devtools.build.lib.skyframe.ActionExecutionInactivityWatchdog; import com.google.devtools.build.lib.skyframe.AspectValue; import com.google.devtools.build.lib.skyframe.Builder; import com.google.devtools.build.lib.skyframe.SkyframeExecutor; import com.google.devtools.build.lib.util.AbruptExitException; import com.google.devtools.build.lib.util.ExitCode; import com.google.devtools.build.lib.util.LoggingUtil; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.vfs.ModifiedFileSet; import com.google.devtools.build.skyframe.CycleInfo; import com.google.devtools.build.skyframe.ErrorInfo; import com.google.devtools.build.skyframe.EvaluationResult; import com.google.devtools.build.skyframe.SkyKey; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import javax.annotation.Nullable; /** * A {@link Builder} implementation driven by Skyframe. */ @VisibleForTesting public class SkyframeBuilder implements Builder { private final SkyframeExecutor skyframeExecutor; private final boolean keepGoing; private final int numJobs; private final boolean finalizeActionsToOutputService; private final ModifiedFileSet modifiedOutputFiles; private final ActionInputFileCache fileCache; private final ActionCacheChecker actionCacheChecker; private final int progressReportInterval; @VisibleForTesting public SkyframeBuilder(SkyframeExecutor skyframeExecutor, ActionCacheChecker actionCacheChecker, boolean keepGoing, int numJobs, ModifiedFileSet modifiedOutputFiles, boolean finalizeActionsToOutputService, ActionInputFileCache fileCache, int progressReportInterval) { this.skyframeExecutor = skyframeExecutor; this.actionCacheChecker = actionCacheChecker; this.keepGoing = keepGoing; this.numJobs = numJobs; this.finalizeActionsToOutputService = finalizeActionsToOutputService; this.modifiedOutputFiles = modifiedOutputFiles; this.fileCache = fileCache; this.progressReportInterval = progressReportInterval; } @Override public void buildArtifacts( Reporter reporter, Set<Artifact> artifacts, Set<ConfiguredTarget> parallelTests, Set<ConfiguredTarget> exclusiveTests, Collection<ConfiguredTarget> targetsToBuild, Collection<AspectValue> aspects, Executor executor, Set<ConfiguredTarget> builtTargets, boolean explain, @Nullable Range<Long> lastExecutionTimeRange, TopLevelArtifactContext topLevelArtifactContext) throws BuildFailedException, AbruptExitException, TestExecException, InterruptedException { skyframeExecutor.prepareExecution(modifiedOutputFiles, lastExecutionTimeRange); skyframeExecutor.setFileCache(fileCache); // Note that executionProgressReceiver accesses builtTargets concurrently (after wrapping in a // synchronized collection), so unsynchronized access to this variable is unsafe while it runs. ExecutionProgressReceiver executionProgressReceiver = new ExecutionProgressReceiver( Preconditions.checkNotNull(builtTargets), countTestActions(exclusiveTests), ImmutableSet.<ConfiguredTarget>builder() .addAll(parallelTests) .addAll(exclusiveTests) .build(), topLevelArtifactContext, skyframeExecutor.getEventBus()); skyframeExecutor .getEventBus() .post(new ExecutionProgressReceiverAvailableEvent(executionProgressReceiver)); List<ExitCode> exitCodes = new LinkedList<>(); EvaluationResult<?> result; ActionExecutionStatusReporter statusReporter = ActionExecutionStatusReporter.create( reporter, executor, skyframeExecutor.getEventBus()); AtomicBoolean isBuildingExclusiveArtifacts = new AtomicBoolean(false); ActionExecutionInactivityWatchdog watchdog = new ActionExecutionInactivityWatchdog( executionProgressReceiver.createInactivityMonitor(statusReporter), executionProgressReceiver.createInactivityReporter(statusReporter, isBuildingExclusiveArtifacts), progressReportInterval); skyframeExecutor.setActionExecutionProgressReportingObjects(executionProgressReceiver, executionProgressReceiver, statusReporter); watchdog.start(); try { result = skyframeExecutor.buildArtifacts( reporter, executor, artifacts, targetsToBuild, aspects, parallelTests, /*exclusiveTesting=*/ false, keepGoing, explain, finalizeActionsToOutputService, numJobs, actionCacheChecker, executionProgressReceiver, topLevelArtifactContext); // progressReceiver is finished, so unsynchronized access to builtTargets is now safe. Optional<ExitCode> exitCode = processResult(reporter, result, keepGoing, skyframeExecutor); Preconditions.checkState( exitCode != null || result.keyNames().size() == (artifacts.size() + targetsToBuild.size() + aspects.size() + parallelTests.size()), "Build reported as successful but not all artifacts and targets built: %s, %s", result, artifacts); if (exitCode != null) { exitCodes.add(exitCode.orNull()); } // Run exclusive tests: either tagged as "exclusive" or is run in an invocation with // --test_output=streamed. isBuildingExclusiveArtifacts.set(true); for (ConfiguredTarget exclusiveTest : exclusiveTests) { // Since only one artifact is being built at a time, we don't worry about an artifact being // built and then the build being interrupted. result = skyframeExecutor.buildArtifacts( reporter, executor, ImmutableSet.<Artifact>of(), targetsToBuild, aspects, ImmutableSet.of(exclusiveTest), /*exclusiveTesting=*/ true, keepGoing, explain, finalizeActionsToOutputService, numJobs, actionCacheChecker, null, topLevelArtifactContext); exitCode = processResult(reporter, result, keepGoing, skyframeExecutor); Preconditions.checkState( exitCode != null || !result.keyNames().isEmpty(), "Build reported as successful but test %s not executed: %s", exclusiveTest, result); if (exitCode != null) { exitCodes.add(exitCode.orNull()); } } } finally { watchdog.stop(); skyframeExecutor.setActionExecutionProgressReportingObjects(null, null, null); statusReporter.unregisterFromEventBus(); } if (!exitCodes.isEmpty()) { if (keepGoing) { // Use the exit code with the highest priority. throw new BuildFailedException( null, Collections.max(exitCodes, ExitCodeComparator.INSTANCE)); } else { throw new BuildFailedException(); } } } /** * Process the Skyframe update, taking into account the keepGoing setting. * * <p>Returns optional {@link ExitCode} based on following conditions: 1. null, if result had no * errors. 2. Optional.absent(), if result had errors but none of the errors specified an exit * code. 3. Optional.of(e), if result had errors and one of them specified exit code 'e'. Throws * on fail-fast failures. */ @Nullable private static Optional<ExitCode> processResult( ExtendedEventHandler eventHandler, EvaluationResult<?> result, boolean keepGoing, SkyframeExecutor skyframeExecutor) throws BuildFailedException, TestExecException { if (result.hasError()) { for (Map.Entry<SkyKey, ErrorInfo> entry : result.errorMap().entrySet()) { Iterable<CycleInfo> cycles = entry.getValue().getCycleInfo(); skyframeExecutor.reportCycles(eventHandler, cycles, entry.getKey()); } if (result.getCatastrophe() != null) { rethrow(result.getCatastrophe()); } if (keepGoing) { // If build fails and keepGoing is true, an exit code is assigned using reported errors // in the following order: // 1. First infrastructure error with non-null exit code // 2. First non-infrastructure error with non-null exit code // 3. Null (later default to 1) ExitCode exitCode = null; for (Map.Entry<SkyKey, ErrorInfo> error : result.errorMap().entrySet()) { Throwable cause = error.getValue().getException(); if (cause instanceof ActionExecutionException) { ActionExecutionException actionExecutionCause = (ActionExecutionException) cause; ExitCode code = actionExecutionCause.getExitCode(); // Update global exit code when current exit code is not null and global exit code has // a lower 'reporting' priority. if (ExitCodeComparator.INSTANCE.compare(code, exitCode) > 0) { exitCode = code; } } } return Optional.fromNullable(exitCode); } ErrorInfo errorInfo = Preconditions.checkNotNull(result.getError(), result); Exception exception = errorInfo.getException(); if (exception == null) { Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()), errorInfo); // If a keepGoing=false build found a cycle, that means there were no other errors thrown // during evaluation (otherwise, it wouldn't have bothered to find a cycle). So the best // we can do is throw a generic build failure exception, since we've already reported the // cycles above. throw new BuildFailedException(null, /*hasCatastrophe=*/ false); } else { rethrow(exception); } } return null; } /** Figure out why an action's execution failed and rethrow the right kind of exception. */ @VisibleForTesting public static void rethrow(Throwable cause) throws BuildFailedException, TestExecException { Throwable innerCause = cause.getCause(); if (innerCause instanceof TestExecException) { throw (TestExecException) innerCause; } if (cause instanceof ActionExecutionException) { ActionExecutionException actionExecutionCause = (ActionExecutionException) cause; // Sometimes ActionExecutionExceptions are caused by Actions with no owner. String message = (actionExecutionCause.getLocation() != null) ? (actionExecutionCause.getLocation().print() + " " + cause.getMessage()) : cause.getMessage(); throw new BuildFailedException( message, actionExecutionCause.isCatastrophe(), actionExecutionCause.getAction(), actionExecutionCause.getRootCauses(), /*errorAlreadyShown=*/ !actionExecutionCause.showError(), actionExecutionCause.getExitCode()); } else if (cause instanceof MissingInputFileException) { throw new BuildFailedException(cause.getMessage()); } else if (cause instanceof BuildFileNotFoundException) { // Sadly, this can happen because we may load new packages during input discovery. Any // failures reading those packages shouldn't terminate the build, but in Skyframe they do. LoggingUtil.logToRemote(Level.WARNING, "undesirable loading exception", cause); throw new BuildFailedException(cause.getMessage()); } else if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else if (cause instanceof Error) { throw (Error) cause; } else { // We encountered an exception we don't think we should have encountered. This can indicate // a bug in our code, such as lower level exceptions not being properly handled, or in our // expectations in this method. throw new IllegalArgumentException( "action terminated with " + "unexpected exception: " + cause.getMessage(), cause); } } private static int countTestActions(Iterable<ConfiguredTarget> testTargets) { int count = 0; for (ConfiguredTarget testTarget : testTargets) { count += TestProvider.getTestStatusArtifacts(testTarget).size(); } return count; } /** * A comparator to determine the reporting priority of {@link ExitCode}. * * <p> Priority: infrastructure exit codes > non-infrastructure exit codes > null exit codes. */ private static class ExitCodeComparator implements Comparator<ExitCode> { private static final ExitCodeComparator INSTANCE = new ExitCodeComparator(); @Override public int compare(ExitCode c1, ExitCode c2) { // returns POSITIVE result when the priority of c1 is HIGHER than the priority of c2 return getPriority(c1) - getPriority(c2); } private int getPriority(ExitCode code) { if (code == null) { return 0; } else { return code.isInfrastructureFailure() ? 2 : 1; } } } }