blob: 6e825839f7fc7c37360dc39ef60d0c64bca74f09 [file] [log] [blame]
/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.event.ThrowableConsoleEvent;
import com.facebook.buck.io.MoreFiles;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepRunner;
import com.facebook.buck.util.concurrent.MoreFutures;
import com.facebook.buck.zip.Unzip;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
/**
* A build engine used to build a {@link BuildRule} which also caches the results. If the current
* {@link RuleKey} of the build rules matches the one on disk, it does not do any work. It also
* tries to fetch its output from an {@link ArtifactCache} to avoid doing any computation.
*/
public class CachingBuildEngine implements BuildEngine {
private static final Logger LOG = Logger.get(CachingBuildEngine.class);
/**
* Key for {@link OnDiskBuildInfo} to identify the ABI key for the deps of a build rule.
*/
@VisibleForTesting
public static final String ABI_KEY_FOR_DEPS_ON_DISK_METADATA = "ABI_KEY_FOR_DEPS";
/**
* These are the values returned by {@link #build(BuildContext, BuildRule)}.
* This must always return the same value for the build of each target.
*/
private final ConcurrentMap<BuildTarget, SettableFuture<BuildRuleSuccess>> results =
Maps.newConcurrentMap();
private final ConcurrentMap<BuildTarget, RuleKey> ruleKeys = Maps.newConcurrentMap();
private final long skipLocalBuildDepth;
public CachingBuildEngine(long skipLocalBuildDepth) {
Preconditions.checkArgument(skipLocalBuildDepth >= 0L);
this.skipLocalBuildDepth = skipLocalBuildDepth;
}
@VisibleForTesting
public CachingBuildEngine() {
this(0L);
}
@VisibleForTesting
public SettableFuture<BuildRuleSuccess> createFutureFor(BuildTarget buildTarget) {
SettableFuture<BuildRuleSuccess> newFuture = SettableFuture.create();
SettableFuture<BuildRuleSuccess> result = results.putIfAbsent(
buildTarget,
newFuture);
return result == null ? newFuture : result;
}
@VisibleForTesting
void setBuildRuleResult(
BuildTarget buildTarget,
BuildRuleSuccess success) {
createFutureFor(buildTarget).set(success);
}
@Override
public boolean isRuleBuilt(BuildTarget buildTarget) throws InterruptedException {
SettableFuture<BuildRuleSuccess> resultFuture = results.get(buildTarget);
return resultFuture != null && MoreFutures.isSuccess(resultFuture);
}
@Nullable
@Override
public RuleKey getRuleKey(BuildTarget buildTarget) {
return ruleKeys.get(buildTarget);
}
@Override
public final ListenableFuture<BuildRuleSuccess> build(
final BuildContext context,
final BuildRule rule) {
final SettableFuture<BuildRuleSuccess> newFuture = SettableFuture.create();
SettableFuture<BuildRuleSuccess> existingFuture = results.putIfAbsent(
rule.getBuildTarget(),
newFuture);
// If the future was already in results for this build rule, return what was there.
if (existingFuture != null) {
return existingFuture;
}
// Build all of the deps first and then schedule a callback for this rule to build itself once
// all of those rules are done building.
try {
// Invoke every dep's build() method and create an uber-ListenableFuture that represents the
// successful completion of all deps.
List<ListenableFuture<BuildRuleSuccess>> builtDeps =
Lists.newArrayListWithCapacity(rule.getDeps().size());
for (BuildRule dep : rule.getDeps()) {
builtDeps.add(build(context, dep));
}
ListenableFuture<List<BuildRuleSuccess>> allBuiltDeps = Futures.allAsList(builtDeps);
// Schedule this rule to build itself once all of the deps are built.
context.getStepRunner().addCallback(allBuiltDeps,
new FutureCallback<List<BuildRuleSuccess>>() {
private final BuckEventBus eventBus = context.getEventBus();
private final OnDiskBuildInfo onDiskBuildInfo = context.createOnDiskBuildInfoFor(
rule.getBuildTarget());
/**
* It is imperative that:
* <ol>
* <li>The {@link BuildInfoRecorder} is not constructed until all of the
* {@link BuildRule}'s {@code deps} are guaranteed to be built. This ensures that
* the {@link RuleKey} will be available before the {@link BuildInfoRecorder} is
* constructed.
* <p>
* This is why a {@link Supplier} is used.
* <li>Only one {@link BuildInfoRecorder} is created per {@link BuildRule}. This
* ensures that all build-related information for a {@link BuildRule} goes though
* a single recorder, whose data will be persisted in {@link #onSuccess(List)}.
* <p>
* This is why {@link Suppliers#memoize(Supplier)} is used.
* </ol>
*/
private final Supplier<BuildInfoRecorder> buildInfoRecorder = Suppliers.memoize(
new Supplier<BuildInfoRecorder>() {
@Override
public BuildInfoRecorder get() {
RuleKey ruleKey;
RuleKey ruleKeyWithoutDeps;
ruleKey = rule.getRuleKey();
ruleKeyWithoutDeps = rule.getRuleKeyWithoutDeps();
return context.createBuildInfoRecorder(
rule.getBuildTarget(), ruleKey, ruleKeyWithoutDeps);
}
});
private boolean startOfBuildWasRecordedOnTheEventBus = false;
@Override
public void onSuccess(List<BuildRuleSuccess> deps) {
// Record the start of the build.
eventBus.logVerboseAndPost(LOG, BuildRuleEvent.started(rule));
startOfBuildWasRecordedOnTheEventBus = true;
BuildResult result = null;
try {
ruleKeys.putIfAbsent(rule.getBuildTarget(), rule.getRuleKey());
result = buildOnceDepsAreBuilt(
rule,
context,
onDiskBuildInfo,
buildInfoRecorder.get(),
shouldTryToFetchFromCache(rule));
if (result.getStatus() == BuildRuleStatus.SUCCESS) {
recordBuildRuleSuccess(result);
}
} catch (InterruptedException | RuntimeException e) {
// StepRunner#addCallback doesn't return a future which means that when we add a
// callback to the step runner in CachingBuildEngine#build, there is no way to
// have the exception thrown in the callback to be forwarded to the future we
// return from it.
// For now, we'll just catch the RuntimeException.
// TODO(simons, t5597862): Consider modifying StepRunner#addCallback
result = new BuildResult(e);
}
if (result.getStatus() == BuildRuleStatus.FAIL) {
recordBuildRuleFailure(result);
// Reset interrupted flag once failure has been recorded.
if (result.getFailure() instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}
private void recordBuildRuleSuccess(BuildResult result)
throws InterruptedException {
// Make sure that all of the local files have the same values they would as if the
// rule had been built locally.
BuildRuleSuccess.Type success = result.getSuccess();
if (success != null && success.shouldWriteRecordedMetadataToDiskAfterBuilding()) {
try {
boolean clearExistingMetadata = success.shouldClearAndOverwriteMetadataOnDisk();
buildInfoRecorder.get().writeMetadataToDisk(clearExistingMetadata);
} catch (IOException e) {
eventBus.post(ThrowableConsoleEvent.create(
e,
"Failed to write metadata to disk for %s.",
rule));
onFailure(e);
}
}
doHydrationAfterBuildStepsFinish(rule, result, onDiskBuildInfo);
// Do the post to the event bus immediately after the future is set so that the
// build time measurement is as accurate as possible.
logBuildRuleFinished(result);
// Only now that the rule should be in a completely valid state, resolve the future.
BuildRuleSuccess buildRuleSuccess = new BuildRuleSuccess(rule, result.getSuccess());
newFuture.set(buildRuleSuccess);
// Finally, upload to the artifact cache.
if (success != null && success.shouldUploadResultingArtifact()) {
buildInfoRecorder.get().performUploadToArtifactCache(context.getArtifactCache(),
eventBus);
}
}
@Override
public void onFailure(Throwable failure) {
recordBuildRuleFailure(new BuildResult(failure));
}
private void recordBuildRuleFailure(BuildResult result) {
// TODO(mbolin): Delete all files produced by the rule, as they are not guaranteed to
// be valid at this point?
try {
onDiskBuildInfo.deleteExistingMetadata();
} catch (IOException e) {
eventBus.post(ThrowableConsoleEvent.create(
e,
"Error when deleting metadata for %s.",
rule));
}
// Note that startOfBuildWasRecordedOnTheEventBus will be false if onSuccess() was
// never invoked.
if (startOfBuildWasRecordedOnTheEventBus) {
logBuildRuleFinished(result);
}
// It seems possible (albeit unlikely) that something could go wrong in
// recordBuildRuleSuccess() after buildRuleResult has been resolved such that Buck
// would attempt to resolve the future again, which would fail.
newFuture.setException(Preconditions.checkNotNull(result.getFailure()));
}
private void logBuildRuleFinished(BuildResult result) {
eventBus.logVerboseAndPost(
LOG,
BuildRuleEvent.finished(
rule,
result.getStatus(),
result.getCacheResult(),
Optional.fromNullable(result.getSuccess())));
}
});
} catch (Throwable failure) {
// This is a defensive catch block: if buildRuleResult is never satisfied, then Buck will
// hang because a callback that is waiting for this rule's future to complete will never be
// executed.
newFuture.setException(failure);
}
return newFuture;
}
/**
* This method is invoked once all of this rule's dependencies are built.
* <p>
* This method should be executed on a fresh Runnable in BuildContext's ListeningExecutorService,
* so there is no reason to schedule new work in a new Runnable.
* <p>
* All exit paths through this method should resolve {@link #results} before exiting. To
* that end, this method should never throw an exception, or else Buck will hang waiting for
* {@link #results} to be resolved.
*
* @param shouldTryToFetchFromCache Making requests to Cassandra can be expensive, so we do not
* attempt to fetch from the cache if any of the transitive dependencies gets rebuilt.
*/
private BuildResult buildOnceDepsAreBuilt(BuildRule rule,
final BuildContext context,
OnDiskBuildInfo onDiskBuildInfo,
BuildInfoRecorder buildInfoRecorder,
boolean shouldTryToFetchFromCache) {
// Compute the current RuleKey and compare it to the one stored on disk.
RuleKey ruleKey = rule.getRuleKey();
Optional<RuleKey> cachedRuleKey = onDiskBuildInfo.getRuleKey();
// If the RuleKeys match, then there is nothing to build.
if (ruleKey.equals(cachedRuleKey.orNull())) {
return new BuildResult(BuildRuleSuccess.Type.MATCHING_RULE_KEY,
CacheResult.LOCAL_KEY_UNCHANGED_HIT);
}
// Deciding whether we need to rebuild is tricky business. We want to rebuild as little as
// possible while always being sound.
//
// For java_library rules that depend only on their first-order deps,
// they only need to rebuild themselves if any of the following conditions hold:
// (1) The definition of the build rule has changed.
// (2) Any of the input files (which includes resources as well as .java files) have changed.
// (3) The ABI of any of its dependent java_library rules has changed.
//
// For other types of build rules, we have to be more conservative when rebuilding. In those
// cases, we rebuild if any of the following conditions hold:
// (1) The definition of the build rule has changed.
// (2) Any of the input files have changed.
// (3) Any of the RuleKeys of this rule's deps have changed.
//
// Because a RuleKey for a rule will change if any of its transitive deps have changed, that
// means a change in one of the leaves can result in almost all rules being rebuilt, which is
// slow. Fortunately, we limit the effects of this when building Java code when checking the ABI
// of deps instead of the RuleKey for deps.
AbiRule abiRule = checkIfRuleOrBuildableIsAbiRule(rule);
if (abiRule != null) {
RuleKey ruleKeyNoDeps = rule.getRuleKeyWithoutDeps();
Optional<RuleKey> cachedRuleKeyNoDeps = onDiskBuildInfo.getRuleKeyWithoutDeps();
if (ruleKeyNoDeps.equals(cachedRuleKeyNoDeps.orNull())) {
// The RuleKey for the definition of this build rule and its input files has not changed.
// Therefore, if the ABI of its deps has not changed, there is nothing to rebuild.
Sha1HashCode abiKeyForDeps = abiRule.getAbiKeyForDeps();
Optional<Sha1HashCode> cachedAbiKeyForDeps = onDiskBuildInfo.getHash(
ABI_KEY_FOR_DEPS_ON_DISK_METADATA);
if (abiKeyForDeps.equals(cachedAbiKeyForDeps.orNull())) {
return new BuildResult(BuildRuleSuccess.Type.MATCHING_DEPS_ABI_AND_RULE_KEY_NO_DEPS,
CacheResult.LOCAL_KEY_UNCHANGED_HIT);
}
}
}
CacheResult cacheResult;
if (shouldTryToFetchFromCache) {
// Before deciding to build, check the ArtifactCache.
// The fetched file is now a ZIP file, so it needs to be unzipped.
try {
cacheResult = tryToFetchArtifactFromBuildCacheAndOverlayOnTopOfProjectFilesystem(
rule,
buildInfoRecorder,
context.getArtifactCache(),
context.getProjectRoot(),
context);
} catch (InterruptedException e) {
return new BuildResult(e);
}
} else {
cacheResult = CacheResult.SKIP;
}
// Run the steps to build this rule since it was not found in the cache.
if (cacheResult.isSuccess()) {
return new BuildResult(BuildRuleSuccess.Type.FETCHED_FROM_CACHE, cacheResult);
}
// The only remaining option is to build locally.
try {
executeCommandsNowThatDepsAreBuilt(rule, context, buildInfoRecorder);
} catch (Exception e) {
// If the build fails, delete all of the on disk metadata.
return new BuildResult(e);
}
return new BuildResult(BuildRuleSuccess.Type.BUILT_LOCALLY, cacheResult);
}
/**
* @return whether we found a local build chain of the given depth by recursing down the
* dependency chain.
*/
private boolean hasLocalBuildChain(BuildRule rule, long depth) {
// Look up the success result for this `BuildRule`.
BuildRuleSuccess success;
try {
success = Preconditions.checkNotNull(getBuildRuleResult(rule.getBuildTarget()));
} catch (InterruptedException | ExecutionException e) {
// This shouldn't ever happen, as the only way we can get to this point is if the
// previous build rules in the dep tree generated results.
throw new IllegalStateException(e);
}
// If we built this locally, and caching is enabled for this rule, it means we likely had
// a cache miss, and may have a local build chain for the given depth.
if (success.getType() == BuildRuleSuccess.Type.BUILT_LOCALLY &&
rule.getCacheMode() == CacheMode.ENABLED) {
// If the given `depth` is zero, we've found our local build chain, so return true.
if (depth == 0) {
return true;
// Otherwise, recurse on our deps looking for the local build chain.
} else {
for (BuildRule dep : rule.getDeps()) {
if (hasLocalBuildChain(dep, depth - 1)) {
return true;
}
}
}
}
return false;
}
/**
* Returns {@code true} if none of the {@link BuildRuleSuccess} objects are built locally.
*/
private boolean shouldTryToFetchFromCache(BuildRule rule) {
// If this rule explicitly disables caching, we won't try to fetch.
if (rule.getCacheMode() == CacheMode.DISABLED) {
return false;
}
// Otherwise, look for a sequence of local builds, which we use as a heuristic to avoid
// fetching this rule from cache, since this will likely result in a miss.
if (skipLocalBuildDepth > 0) {
for (BuildRule dep : rule.getDeps()) {
if (hasLocalBuildChain(dep, skipLocalBuildDepth - 1)) {
return false;
}
}
}
return true;
}
private CacheResult tryToFetchArtifactFromBuildCacheAndOverlayOnTopOfProjectFilesystem(
BuildRule rule,
BuildInfoRecorder buildInfoRecorder,
ArtifactCache artifactCache,
Path projectRoot,
BuildContext buildContext) throws InterruptedException {
// Create a temp file whose extension must be ".zip" for Filesystems.newFileSystem() to infer
// that we are creating a zip-based FileSystem.
File zipFile;
try {
zipFile = File.createTempFile(
MoreFiles.sanitize(rule.getFullyQualifiedName()),
".zip");
} catch (IOException e) {
throw new RuntimeException(e);
}
// TODO(mbolin): Change ArtifactCache.fetch() so that it returns a File instead of takes one.
// Then we could download directly from Cassandra into the on-disk cache and unzip it from
// there.
CacheResult cacheResult = buildInfoRecorder.fetchArtifactForBuildable(zipFile, artifactCache);
if (!cacheResult.isSuccess()) {
zipFile.delete();
return cacheResult;
}
// We unzip the file in the root of the project directory.
// Ideally, the following would work:
//
// Path pathToZip = Paths.get(zipFile.getAbsolutePath());
// FileSystem fs = FileSystems.newFileSystem(pathToZip, /* loader */ null);
// Path root = Iterables.getOnlyElement(fs.getRootDirectories());
// MoreFiles.copyRecursively(root, projectRoot);
//
// Unfortunately, this does not appear to work, in practice, because MoreFiles fails when trying
// to resolve a Path for a zip entry against a file Path on disk.
try {
Unzip.extractZipFile(zipFile.toPath().toAbsolutePath(),
projectRoot.toAbsolutePath(),
/* overwriteExistingFiles */ true);
} catch (IOException e) {
// In the wild, we have seen some inexplicable failures during this step. For now, we try to
// give the user as much information as we can to debug the issue, but return CacheResult.MISS
// so that Buck will fall back on doing a local build.
buildContext.getEventBus().post(ConsoleEvent.warning(
"Failed to unzip the artifact for %s at %s.\n" +
"The rule will be built locally, " +
"but here is the stacktrace of the failed unzip call:\n" +
rule.getBuildTarget(),
zipFile.getAbsolutePath(),
Throwables.getStackTraceAsString(e)));
return CacheResult.MISS;
}
// We only delete the ZIP file when it has been unzipped successfully. Otherwise, we leave it
// around for debugging purposes.
zipFile.delete();
return cacheResult;
}
/**
* Execute the commands for this build rule. Requires all dependent rules are already built
* successfully.
*/
private void executeCommandsNowThatDepsAreBuilt(
BuildRule rule,
BuildContext context,
BuildInfoRecorder buildInfoRecorder)
throws Exception {
LOG.debug("Building locally: %s", rule);
// Get and run all of the commands.
BuildableContext buildableContext = new DefaultBuildableContext(
buildInfoRecorder);
List<Step> steps = rule.getBuildSteps(context, buildableContext);
AbiRule abiRule = checkIfRuleOrBuildableIsAbiRule(rule);
if (abiRule != null) {
buildableContext.addMetadata(
ABI_KEY_FOR_DEPS_ON_DISK_METADATA,
abiRule.getAbiKeyForDeps().getHash());
}
StepRunner stepRunner = context.getStepRunner();
for (Step step : steps) {
stepRunner.runStepForBuildTarget(step, rule.getBuildTarget());
// Check for interruptions that may have been ignored by step.
if (Thread.interrupted()) {
Thread.currentThread().interrupt();
throw new InterruptedException();
}
}
LOG.debug("Build completed: %s", rule);
}
@VisibleForTesting
public void doHydrationAfterBuildStepsFinish(
BuildRule rule,
BuildResult result,
OnDiskBuildInfo onDiskBuildInfo) {
// Give the rule a chance to populate its internal data structures now that all of the
// files should be in a valid state.
InitializableFromDisk<?> initializable = deriveInitializable(rule);
if (initializable != null) {
doInitializeFromDisk(initializable, onDiskBuildInfo);
}
// Only now that the rule should be in a completely valid state, resolve the future.
BuildRuleSuccess buildRuleSuccess = new BuildRuleSuccess(rule, result.getSuccess());
results.get(rule.getBuildTarget()).set(buildRuleSuccess);
}
/**
* We're moving to a world where BuildRule becomes Buildable. In many cases, that refactoring
* hasn't happened yet, and in that case "self" and self's Buildable are the same instance. We
* don't want to call initializeFromDisk more than once, so handle this case gracefully-ish.
*/
@Nullable
private InitializableFromDisk<?> deriveInitializable(BuildRule rule) {
if (rule instanceof InitializableFromDisk) {
return (InitializableFromDisk<?>) rule;
}
return null;
}
private <T> void doInitializeFromDisk(InitializableFromDisk<T> initializable,
OnDiskBuildInfo onDiskBuildInfo) {
BuildOutputInitializer<T> buildOutputInitializer = initializable.getBuildOutputInitializer();
T buildOutput = buildOutputInitializer.initializeFromDisk(onDiskBuildInfo);
buildOutputInitializer.setBuildOutput(buildOutput);
}
@Nullable
@Override
public BuildRuleSuccess getBuildRuleResult(BuildTarget buildTarget)
throws ExecutionException, InterruptedException {
SettableFuture<BuildRuleSuccess> result = results.get(buildTarget);
if (result == null) {
return null;
}
return result.get();
}
@Nullable
private AbiRule checkIfRuleOrBuildableIsAbiRule(BuildRule rule) {
if (rule instanceof AbiRule) {
return (AbiRule) rule;
}
return null;
}
}