blob: 6189cfc97c750d521a7973d47fda2d0edca03c15 [file] [log] [blame]
// Copyright 2014 Google Inc. 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.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.actions.Action;
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.ResourceManager;
import com.google.devtools.build.lib.actions.TestExecException;
import com.google.devtools.build.lib.analysis.AspectCompleteEvent;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
import com.google.devtools.build.lib.events.EventHandler;
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.ActionExecutionValue;
import com.google.devtools.build.lib.skyframe.AspectCompletionValue;
import com.google.devtools.build.lib.skyframe.AspectValue;
import com.google.devtools.build.lib.skyframe.Builder;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.skyframe.TargetCompletionValue;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.BlazeClock;
import com.google.devtools.build.lib.util.LoggingUtil;
import com.google.devtools.build.skyframe.CycleInfo;
import com.google.devtools.build.skyframe.ErrorInfo;
import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
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 checkOutputFiles;
private final ActionInputFileCache fileCache;
private final ActionCacheChecker actionCacheChecker;
private final int progressReportInterval;
@VisibleForTesting
public SkyframeBuilder(SkyframeExecutor skyframeExecutor, ActionCacheChecker actionCacheChecker,
boolean keepGoing, int numJobs, boolean checkOutputFiles,
ActionInputFileCache fileCache, int progressReportInterval) {
this.skyframeExecutor = skyframeExecutor;
this.actionCacheChecker = actionCacheChecker;
this.keepGoing = keepGoing;
this.numJobs = numJobs;
this.checkOutputFiles = checkOutputFiles;
this.fileCache = fileCache;
this.progressReportInterval = progressReportInterval;
}
@Override
public void buildArtifacts(
EventHandler eventHandler,
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)
throws BuildFailedException, AbruptExitException, TestExecException, InterruptedException {
skyframeExecutor.prepareExecution(checkOutputFiles, 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), skyframeExecutor.getEventBus());
ResourceManager.instance().setEventBus(skyframeExecutor.getEventBus());
boolean success = false;
EvaluationResult<?> result;
ActionExecutionStatusReporter statusReporter = ActionExecutionStatusReporter.create(
eventHandler, 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(
eventHandler,
executor,
artifacts,
targetsToBuild,
aspects,
parallelTests,
/*exclusiveTesting=*/ false,
keepGoing,
explain,
numJobs,
actionCacheChecker,
executionProgressReceiver);
// progressReceiver is finished, so unsynchronized access to builtTargets is now safe.
success = processResult(eventHandler, result, keepGoing, skyframeExecutor);
Preconditions.checkState(
!success
|| 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);
// 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(
eventHandler,
executor,
ImmutableSet.<Artifact>of(),
targetsToBuild,
aspects,
ImmutableSet.of(exclusiveTest), /*exclusiveTesting=*/
true,
keepGoing,
explain,
numJobs,
actionCacheChecker,
null);
boolean exclusiveSuccess = processResult(eventHandler, result, keepGoing, skyframeExecutor);
Preconditions.checkState(!exclusiveSuccess || !result.keyNames().isEmpty(),
"Build reported as successful but test %s not executed: %s",
exclusiveTest, result);
success &= exclusiveSuccess;
}
} finally {
watchdog.stop();
ResourceManager.instance().unsetEventBus();
skyframeExecutor.setActionExecutionProgressReportingObjects(null, null, null);
statusReporter.unregisterFromEventBus();
}
if (!success) {
throw new BuildFailedException();
}
}
private static boolean resultHasCatastrophicError(EvaluationResult<?> result) {
for (ErrorInfo errorInfo : result.errorMap().values()) {
if (errorInfo.isCatastrophic()) {
return true;
}
}
// An unreported catastrophe manifests with hasError() being true but no errors visible.
return result.hasError() && result.errorMap().isEmpty();
}
/**
* Process the Skyframe update, taking into account the keepGoing setting.
*
* <p>Returns false if the update() failed, but we should continue. Returns true on success.
* Throws on fail-fast failures.
*/
private static boolean processResult(EventHandler eventHandler, EvaluationResult<?> result,
boolean keepGoing, SkyframeExecutor skyframeExecutor)
throws BuildFailedException, TestExecException {
if (result.hasError()) {
boolean hasCycles = false;
for (Map.Entry<SkyKey, ErrorInfo> entry : result.errorMap().entrySet()) {
Iterable<CycleInfo> cycles = entry.getValue().getCycleInfo();
skyframeExecutor.reportCycles(eventHandler, cycles, entry.getKey());
hasCycles |= !Iterables.isEmpty(cycles);
}
if (keepGoing && !resultHasCatastrophicError(result)) {
return false;
}
if (hasCycles || result.errorMap().isEmpty()) {
// error map may be empty in the case of a catastrophe.
throw new BuildFailedException();
} else {
rethrow(Preconditions.checkNotNull(result.getError().getException()));
}
}
return true;
}
/** 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());
} 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;
}
/**
* Listener for executed actions and built artifacts. We use a listener so that we have an
* accurate set of successfully run actions and built artifacts, even if the build is interrupted.
*/
private static final class ExecutionProgressReceiver implements EvaluationProgressReceiver,
SkyframeActionExecutor.ProgressSupplier, SkyframeActionExecutor.ActionCompletedReceiver {
private static final NumberFormat PROGRESS_MESSAGE_NUMBER_FORMATTER;
// Must be thread-safe!
private final Set<ConfiguredTarget> builtTargets;
private final Set<SkyKey> enqueuedActions = Sets.newConcurrentHashSet();
private final Set<Action> completedActions = Sets.newConcurrentHashSet();
private final Object activityIndicator = new Object();
/** Number of exclusive tests. To be accounted for in progress messages. */
private final int exclusiveTestsCount;
private final EventBus eventBus;
static {
PROGRESS_MESSAGE_NUMBER_FORMATTER = NumberFormat.getIntegerInstance(Locale.ENGLISH);
PROGRESS_MESSAGE_NUMBER_FORMATTER.setGroupingUsed(true);
}
/**
* {@code builtTargets} is accessed through a synchronized set, and so no other access to it
* is permitted while this receiver is active.
*/
ExecutionProgressReceiver(Set<ConfiguredTarget> builtTargets, int exclusiveTestsCount,
EventBus eventBus) {
this.builtTargets = Collections.synchronizedSet(builtTargets);
this.exclusiveTestsCount = exclusiveTestsCount;
this.eventBus = eventBus;
}
@Override
public void invalidated(SkyKey skyKey, InvalidationState state) {}
@Override
public void enqueueing(SkyKey skyKey) {
if (ActionExecutionValue.isReportWorthyAction(skyKey)) {
// Remember all enqueued actions for the benefit of progress reporting.
// We discover most actions early in the build, well before we start executing them.
// Some of these will be cache hits and won't be executed, so we'll need to account for them
// in the evaluated method too.
enqueuedActions.add(skyKey);
}
}
@Override
public void computed(SkyKey skyKey, long elapsedTimeNanos) {}
@Override
public void evaluated(SkyKey skyKey, Supplier<SkyValue> skyValueSupplier,
EvaluationState state) {
SkyFunctionName type = skyKey.functionName();
if (type.equals(SkyFunctions.TARGET_COMPLETION)) {
TargetCompletionValue value = (TargetCompletionValue) skyValueSupplier.get();
if (value != null) {
ConfiguredTarget target = value.getConfiguredTarget();
builtTargets.add(target);
eventBus.post(TargetCompleteEvent.createSuccessful(target));
}
} else if (type.equals(SkyFunctions.ASPECT_COMPLETION)) {
AspectCompletionValue value = (AspectCompletionValue) skyValueSupplier.get();
if (value != null) {
eventBus.post(AspectCompleteEvent.createSuccessful(value.getAspectValue()));
}
} else if (type.equals(SkyFunctions.ACTION_EXECUTION)) {
// Remember all completed actions, even those in error, regardless of having been cached or
// really executed.
actionCompleted((Action) skyKey.argument());
}
}
/**
* {@inheritDoc}
*
* <p>This method adds the action to {@link #completedActions} and notifies the
* {@link #activityIndicator}.
*
* <p>We could do this only in the {@link #evaluated} method too, but as it happens the action
* executor tells the reporter about the completed action before the node is inserted into the
* graph, so the reporter would find out about the completed action sooner than we could
* have updated {@link #completedActions}, which would result in incorrect numbers on the
* progress messages. However we have to store completed actions in {@link #evaluated} too,
* because that's the only place we get notified about completed cached actions.
*/
@Override
public void actionCompleted(Action a) {
if (ActionExecutionValue.isReportWorthyAction(a)) {
completedActions.add(a);
synchronized (activityIndicator) {
activityIndicator.notifyAll();
}
}
}
@Override
public String getProgressString() {
return String.format("[%s / %s]",
PROGRESS_MESSAGE_NUMBER_FORMATTER.format(completedActions.size()),
PROGRESS_MESSAGE_NUMBER_FORMATTER.format(exclusiveTestsCount + enqueuedActions.size()));
}
ActionExecutionInactivityWatchdog.InactivityMonitor createInactivityMonitor(
final ActionExecutionStatusReporter statusReporter) {
return new ActionExecutionInactivityWatchdog.InactivityMonitor() {
@Override
public boolean hasStarted() {
return !enqueuedActions.isEmpty();
}
@Override
public int getPending() {
return statusReporter.getCount();
}
@Override
public int waitForNextCompletion(int timeoutMilliseconds) throws InterruptedException {
synchronized (activityIndicator) {
int before = completedActions.size();
long startTime = BlazeClock.instance().currentTimeMillis();
while (true) {
activityIndicator.wait(timeoutMilliseconds);
int completed = completedActions.size() - before;
long now = 0;
if (completed > 0 || (startTime + timeoutMilliseconds) <= (now = BlazeClock.instance()
.currentTimeMillis())) {
// Some actions completed, or timeout fully elapsed.
return completed;
} else {
// Spurious Wakeup -- no actions completed and there's still time to wait.
timeoutMilliseconds -= now - startTime; // account for elapsed wait time
startTime = now;
}
}
}
}
};
}
ActionExecutionInactivityWatchdog.InactivityReporter createInactivityReporter(
final ActionExecutionStatusReporter statusReporter,
final AtomicBoolean isBuildingExclusiveArtifacts) {
return new ActionExecutionInactivityWatchdog.InactivityReporter() {
@Override
public void maybeReportInactivity() {
// Do not report inactivity if we are currently running an exclusive test or a streaming
// action (in practice only tests can stream and it implicitly makes them exclusive).
if (!isBuildingExclusiveArtifacts.get()) {
statusReporter.showCurrentlyExecutingActions(
ExecutionProgressReceiver.this.getProgressString() + " ");
}
}
};
}
}
}