aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java279
1 files changed, 220 insertions, 59 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
index be264f768d..3d845f6858 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
@@ -16,10 +16,12 @@ package com.google.devtools.build.lib.skyframe;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFile;
import com.google.devtools.build.lib.actions.cache.Digest;
import com.google.devtools.build.lib.actions.cache.DigestUtils;
import com.google.devtools.build.lib.actions.cache.Metadata;
@@ -30,12 +32,15 @@ import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter;
import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.lib.vfs.Symlinks;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -54,22 +59,67 @@ import javax.annotation.Nullable;
* ways. First, it is served as requested during action execution, primarily by the {@code
* ActionCacheChecker} when determining if the action must be rerun, and then after the action is
* run, to gather information about the outputs. Second, it is accessed by {@link
- * ArtifactFunction}s in order to construct {@link ArtifactValue}s. Third, the {@link
+ * ArtifactFunction}s in order to construct {@link ArtifactValue}, and by this class itself to
+ * generate {@link TreeArtifactValue}s. Third, the {@link
* FilesystemValueChecker} uses it to determine the set of output files to check for inter-build
* modifications. Because all these use cases are slightly different, we must occasionally store two
- * versions of the data for a value (see {@link #getAdditionalOutputData} for more.
+ * versions of the data for a value. See {@link #getAdditionalOutputData} for elaboration on
+ * the difference between these cases, and see the javadoc for the various internal maps to see
+ * what is stored where.
*/
@VisibleForTesting
public class ActionMetadataHandler implements MetadataHandler {
- /** This should never be read directly. Use {@link #getInputFileArtifactValue} instead. */
+
+ /**
+ * Data for input artifacts. Immutable.
+ *
+ * <p>This should never be read directly. Use {@link #getInputFileArtifactValue} instead.</p>
+ */
private final Map<Artifact, FileArtifactValue> inputArtifactData;
- private final ConcurrentMap<Artifact, FileValue> outputArtifactData =
+
+ /** FileValues for each output ArtifactFile. */
+ private final ConcurrentMap<ArtifactFile, FileValue> outputArtifactFileData =
new ConcurrentHashMap<>();
+
+ /**
+ * Maps output TreeArtifacts to their contents. These maps are either injected or read
+ * directly from the filesystem.
+ * If the value is null, this means nothing was injected, and the output TreeArtifact
+ * is to have its values read from disk instead.
+ */
+ private final ConcurrentMap<Artifact, Set<ArtifactFile>> outputDirectoryListings =
+ new ConcurrentHashMap<>();
+
+ /** Outputs that are to be omitted. */
private final Set<Artifact> omittedOutputs = Sets.newConcurrentHashSet();
- // See #getAdditionalOutputData for documentation of this field.
+
+ /**
+ * Contains RealArtifactValues when those values must be stored separately.
+ * See {@link #getAdditionalOutputData()} for details.
+ */
private final ConcurrentMap<Artifact, FileArtifactValue> additionalOutputData =
new ConcurrentHashMap<>();
- private final Set<Artifact> injectedArtifacts = Sets.newConcurrentHashSet();
+
+ /**
+ * Contains per-fragment FileArtifactValues when those values must be stored separately.
+ * Bona-fide Artifacts are stored in {@link #additionalOutputData} instead.
+ * See {@link #getAdditionalOutputData()} for details.
+ * Unlike additionalOutputData, this map is discarded (the relevant FileArtifactValues
+ * are stored in outputTreeArtifactData's values instead).
+ */
+ private final ConcurrentMap<ArtifactFile, FileArtifactValue> cachedTreeArtifactFileData =
+ new ConcurrentHashMap<>();
+
+ /**
+ * Data for TreeArtifactValues, constructed from outputArtifactFileData and
+ * additionalOutputFileData.
+ */
+ private final ConcurrentMap<Artifact, TreeArtifactValue> outputTreeArtifactData =
+ new ConcurrentHashMap<>();
+
+ /** Tracks which ArtifactFiles have had metadata injected. */
+ private final Set<ArtifactFile> injectedFiles = Sets.newConcurrentHashSet();
+
private final ImmutableSet<Artifact> outputs;
private final TimestampGranularityMonitor tsgm;
@@ -121,7 +171,11 @@ public class ActionMetadataHandler implements MetadataHandler {
}
/**
- * We cache data for constant-metadata artifacts, even though it is technically unnecessary,
+ * Get the real (viz. on-disk) metadata for an Artifact.
+ * A key assumption is that getRealMetadata() will be called for every Artifact in this
+ * ActionMetadataHandler, to populate additionalOutputData and outputTreeArtifactData.
+ *
+ * <p>We cache data for constant-metadata artifacts, even though it is technically unnecessary,
* because the data stored in this cache is consumed by various parts of Blaze via the {@link
* ActionExecutionValue} (for now, {@link FilesystemValueChecker} and {@link ArtifactFunction}).
* It is simpler for those parts if every output of the action is present in the cache. However,
@@ -148,8 +202,17 @@ public class ActionMetadataHandler implements MetadataHandler {
FileArtifactValue oldValue = additionalOutputData.putIfAbsent(artifact, value);
checkInconsistentData(artifact, oldValue, value);
return metadataFromValue(value);
+ } else if (artifact.isTreeArtifact()) {
+ TreeArtifactValue setValue = getTreeArtifactValue(artifact);
+ if (setValue != null && setValue != TreeArtifactValue.MISSING_TREE_ARTIFACT) {
+ return setValue.getMetadata();
+ }
+ // We use FileNotFoundExceptions to determine if an Artifact was or wasn't found.
+ // Calling code depends on this particular exception.
+ throw new FileNotFoundException(artifact + " not found");
}
- FileValue fileValue = outputArtifactData.get(artifact);
+ // It's an ordinary artifact.
+ FileValue fileValue = outputArtifactFileData.get(artifact);
if (fileValue != null) {
// Non-middleman artifacts should only have additionalOutputData if they have
// outputArtifactData. We don't assert this because of concurrency possibilities, but at least
@@ -167,23 +230,21 @@ public class ActionMetadataHandler implements MetadataHandler {
}
// We do not cache exceptions besides nonexistence here, because it is unlikely that the file
// will be requested from this cache too many times.
- fileValue = fileValueFromArtifact(artifact, null, tsgm);
- FileValue oldFileValue = outputArtifactData.putIfAbsent(artifact, fileValue);
- checkInconsistentData(artifact, oldFileValue, value);
+ fileValue = constructFileValue(artifact, null);
return maybeStoreAdditionalData(artifact, fileValue, null);
}
/**
- * Check that the new {@code data} we just calculated for an {@code artifact} agrees with the
+ * Check that the new {@code data} we just calculated for an {@link ArtifactFile} agrees with the
* {@code oldData} (presumably calculated concurrently), if it was present.
*/
// Not private only because used by SkyframeActionExecutor's metadata handler.
- static void checkInconsistentData(Artifact artifact,
+ static void checkInconsistentData(ArtifactFile file,
@Nullable Object oldData, Object data) throws IOException {
if (oldData != null && !oldData.equals(data)) {
// Another thread checked this file since we looked at the map, and got a different answer
// than we did. Presumably the user modified the file between reads.
- throw new IOException("Data for " + artifact.prettyPrint() + " changed to " + data
+ throw new IOException("Data for " + file.prettyPrint() + " changed to " + data
+ " after it was calculated as " + oldData);
}
}
@@ -193,63 +254,145 @@ public class ActionMetadataHandler implements MetadataHandler {
* for normal (non-middleman) artifacts.
*/
@Nullable
- private Metadata maybeStoreAdditionalData(Artifact artifact, FileValue data,
+ private Metadata maybeStoreAdditionalData(ArtifactFile file, FileValue data,
@Nullable byte[] injectedDigest) throws IOException {
if (!data.exists()) {
// Nonexistent files should only occur before executing an action.
- throw new FileNotFoundException(artifact.prettyPrint() + " does not exist");
+ throw new FileNotFoundException(file.prettyPrint() + " does not exist");
}
- boolean isFile = data.isFile();
- boolean useDigest = DigestUtils.useFileDigest(isFile, isFile ? data.getSize() : 0);
- if (useDigest && data.getDigest() != null) {
- // We do not need to store the FileArtifactValue separately -- the digest is in the file value
- // and that is all that is needed for this file's metadata.
- return new Metadata(data.getDigest());
+ if (file instanceof Artifact) {
+ // Artifacts may use either the "real" digest or the mtime, if the file is size 0.
+ boolean isFile = data.isFile();
+ boolean useDigest = DigestUtils.useFileDigest(isFile, isFile ? data.getSize() : 0);
+ if (useDigest && data.getDigest() != null) {
+ // We do not need to store the FileArtifactValue separately -- the digest is in the
+ // file value and that is all that is needed for this file's metadata.
+ return new Metadata(data.getDigest());
+ }
+ // Unfortunately, the FileValue does not contain enough information for us to calculate the
+ // corresponding FileArtifactValue -- either the metadata must use the modified time, which we
+ // do not expose in the FileValue, or the FileValue didn't store the digest So we store the
+ // metadata separately.
+ // Use the FileValue's digest if no digest was injected, or if the file can't be digested.
+ injectedDigest = injectedDigest != null || !isFile ? injectedDigest : data.getDigest();
+ FileArtifactValue value =
+ FileArtifactValue.create(
+ (Artifact) file, isFile, isFile ? data.getSize() : 0, injectedDigest);
+ FileArtifactValue oldValue = additionalOutputData.putIfAbsent((Artifact) file, value);
+ checkInconsistentData(file, oldValue, value);
+ return metadataFromValue(value);
+ } else {
+ // Non-Artifact ArtifactFiles are always "real" files, and always use the real digest.
+ // When null, createWithDigest() will pull the digest from the filesystem.
+ FileArtifactValue value =
+ FileArtifactValue.createWithDigest(file.getPath(), injectedDigest, data.getSize());
+ FileArtifactValue oldValue = cachedTreeArtifactFileData.putIfAbsent(file, value);
+ checkInconsistentData(file, oldValue, value);
+ return new Metadata(value.getDigest());
}
- // Unfortunately, the FileValue does not contain enough information for us to calculate the
- // corresponding FileArtifactValue -- either the metadata must use the modified time, which we
- // do not expose in the FileValue, or the FileValue didn't store the digest So we store the
- // metadata separately.
- // Use the FileValue's digest if no digest was injected, or if the file can't be digested.
- injectedDigest = injectedDigest != null || !isFile ? injectedDigest : data.getDigest();
- FileArtifactValue value =
- FileArtifactValue.create(artifact, isFile, isFile ? data.getSize() : 0, injectedDigest);
- FileArtifactValue oldValue = additionalOutputData.putIfAbsent(artifact, value);
- checkInconsistentData(artifact, oldValue, value);
- return metadataFromValue(value);
}
@Override
public void setDigestForVirtualArtifact(Artifact artifact, Digest digest) {
- Preconditions.checkState(artifact.isMiddlemanArtifact(), artifact);
+ Preconditions.checkArgument(artifact.isMiddlemanArtifact(), artifact);
Preconditions.checkNotNull(digest, artifact);
additionalOutputData.put(artifact,
- FileArtifactValue.createMiddleman(digest.asMetadata().digest));
+ FileArtifactValue.createProxy(digest.asMetadata().digest));
+ }
+
+ private Set<ArtifactFile> getTreeArtifactContents(Artifact artifact) {
+ Preconditions.checkArgument(artifact.isTreeArtifact(), artifact);
+ Set<ArtifactFile> contents = outputDirectoryListings.get(artifact);
+ if (contents == null) {
+ // Unfortunately, there is no such thing as a ConcurrentHashSet.
+ contents = Collections.newSetFromMap(new ConcurrentHashMap<ArtifactFile, Boolean>());
+ Set<ArtifactFile> oldContents = outputDirectoryListings.putIfAbsent(artifact, contents);
+ // Avoid a race condition.
+ if (oldContents != null) {
+ contents = oldContents;
+ }
+ }
+ return contents;
+ }
+
+ private TreeArtifactValue getTreeArtifactValue(Artifact artifact) throws IOException {
+ TreeArtifactValue value = outputTreeArtifactData.get(artifact);
+ if (value != null) {
+ return value;
+ }
+
+ Set<ArtifactFile> contents = outputDirectoryListings.get(artifact);
+ if (contents != null) {
+ value = constructTreeArtifactValue(contents);
+ } else {
+ // Functionality is planned to construct the TreeArtifactValue from disk here.
+ throw new UnsupportedOperationException();
+ }
+
+ TreeArtifactValue oldValue = outputTreeArtifactData.putIfAbsent(artifact, value);
+ checkInconsistentData(artifact, oldValue, value);
+ return value;
+ }
+
+ private TreeArtifactValue constructTreeArtifactValue(Collection<ArtifactFile> contents)
+ throws IOException {
+ Map<PathFragment, FileArtifactValue> values = Maps.newHashMapWithExpectedSize(contents.size());
+
+ for (ArtifactFile file : contents) {
+ FileArtifactValue cachedValue = cachedTreeArtifactFileData.get(file);
+ if (cachedValue == null) {
+ FileValue fileValue = outputArtifactFileData.get(file);
+ // This is similar to what's present in getRealMetadataForArtifactFile, except
+ // we get back the FileValue, not the metadata.
+ // We do not cache exceptions besides nonexistence here, because it is unlikely that the
+ // file will be requested from this cache too many times.
+ if (fileValue == null) {
+ fileValue = constructFileValue(file, /*statNoFollow=*/ null);
+ // A minor hack: maybeStoreAdditionalData will force the data to be stored
+ // in cachedTreeArtifactFileData.
+ maybeStoreAdditionalData(file, fileValue, null);
+ }
+ cachedValue = Preconditions.checkNotNull(cachedTreeArtifactFileData.get(file), file);
+ }
+
+ values.put(file.getParentRelativePath(), cachedValue);
+ }
+
+ return TreeArtifactValue.create(values);
+ }
+
+ @Override
+ public void addExpandedTreeOutput(ArtifactFile output) {
+ Preconditions.checkArgument(output.getParent().isTreeArtifact(),
+ "Expanded set output must belong to a TreeArtifact");
+ Set<ArtifactFile> values = getTreeArtifactContents(output.getParent());
+ values.add(output);
}
@Override
public void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest) {
- if (output instanceof Artifact) {
- Artifact artifact = (Artifact) output;
- Preconditions.checkState(injectedArtifacts.add(artifact), artifact);
+ // Assumption: any non-ArtifactFile output is 'virtual' and should be ignored here.
+ if (output instanceof ArtifactFile) {
+ final ArtifactFile file = (ArtifactFile) output;
+ Preconditions.checkState(injectedFiles.add(file), file);
FileValue fileValue;
try {
// This call may do an unnecessary call to Path#getFastDigest to see if the digest is
// readily available. We cannot pass the digest in, though, because if it is not available
// from the filesystem, this FileValue will not compare equal to another one created for the
// same file, because the other one will be missing its digest.
- fileValue = fileValueFromArtifact(artifact, FileStatusWithDigestAdapter.adapt(statNoFollow),
- tsgm);
+ fileValue = fileValueFromArtifactFile(file,
+ FileStatusWithDigestAdapter.adapt(statNoFollow), tsgm);
// Ensure the digest supplied matches the actual digest if it exists.
byte[] fileDigest = fileValue.getDigest();
if (fileDigest != null && !Arrays.equals(digest, fileDigest)) {
BaseEncoding base16 = BaseEncoding.base16();
String digestString = (digest != null) ? base16.encode(digest) : "null";
- String fileDigestString = (fileDigest != null) ? base16.encode(fileDigest) : "null";
+ String fileDigestString = base16.encode(fileDigest);
throw new IllegalStateException("Expected digest " + digestString + " for artifact "
- + artifact + ", but got " + fileDigestString + " (" + fileValue + ")");
+ + file + ", but got " + fileDigestString + " (" + fileValue + ")");
}
- outputArtifactData.put(artifact, fileValue);
+ outputArtifactFileData.put(file, fileValue);
} catch (IOException e) {
// Do nothing - we just failed to inject metadata. Real error handling will be done later,
// when somebody will try to access that file.
@@ -259,14 +402,14 @@ public class ActionMetadataHandler implements MetadataHandler {
// the filesystem does not support fast digests. Since we usually only inject digests when
// running with a filesystem that supports fast digests, this is fairly unlikely.
try {
- maybeStoreAdditionalData(artifact, fileValue, digest);
+ maybeStoreAdditionalData(file, fileValue, digest);
} catch (IOException e) {
if (fileValue.getSize() != 0) {
// Empty files currently have their mtimes examined, and so could throw. No other files
// should throw, since all filesystem access has already been done.
throw new IllegalStateException(
"Filesystem should not have been accessed while injecting data for "
- + artifact.prettyPrint(), e);
+ + file.prettyPrint(), e);
}
// Ignore exceptions for empty files, as above.
}
@@ -289,12 +432,15 @@ public class ActionMetadataHandler implements MetadataHandler {
@Override
public void discardOutputMetadata() {
- Preconditions.checkState(injectedArtifacts.isEmpty(),
- "Artifacts cannot be injected before action execution: %s", injectedArtifacts);
+ Preconditions.checkState(injectedFiles.isEmpty(),
+ "Files cannot be injected before action execution: %s", injectedFiles);
Preconditions.checkState(omittedOutputs.isEmpty(),
"Artifacts cannot be marked omitted before action execution: %s", omittedOutputs);
- outputArtifactData.clear();
+ outputArtifactFileData.clear();
+ outputDirectoryListings.clear();
+ outputTreeArtifactData.clear();
additionalOutputData.clear();
+ cachedTreeArtifactFileData.clear();
}
@Override
@@ -315,21 +461,26 @@ public class ActionMetadataHandler implements MetadataHandler {
}
@Override
- public boolean isInjected(Artifact artifact) {
- return injectedArtifacts.contains(artifact);
+ public boolean isInjected(ArtifactFile file) {
+ return injectedFiles.contains(file);
+ }
+
+ /** @return data for output files that was computed during execution. */
+ Map<ArtifactFile, FileValue> getOutputArtifactFileData() {
+ return outputArtifactFileData;
}
/**
- * @return data for output files that was computed during execution. Should include data for all
- * non-middleman artifacts.
+ * @return data for TreeArtifacts that was computed during execution. May contain copies of
+ * {@link TreeArtifactValue#MISSING_TREE_ARTIFACT}.
*/
- Map<Artifact, FileValue> getOutputData() {
- return outputArtifactData;
+ Map<Artifact, TreeArtifactValue> getOutputTreeArtifactData() {
+ return outputTreeArtifactData;
}
/**
* Returns data for any output files whose metadata was not computable from the corresponding
- * entry in {@link #getOutputData}.
+ * entry in {@link #getOutputArtifactFileData}.
*
* <p>There are three reasons why we might not be able to compute metadata for an artifact from
* the FileValue. First, middleman artifacts have no corresponding FileValues. Second, if
@@ -346,12 +497,22 @@ public class ActionMetadataHandler implements MetadataHandler {
return additionalOutputData;
}
- static FileValue fileValueFromArtifact(Artifact artifact,
+ /** Constructs a new FileValue, saves it, and checks inconsistent data. */
+ FileValue constructFileValue(ArtifactFile file, @Nullable FileStatusWithDigest statNoFollow)
+ throws IOException {
+ FileValue value = fileValueFromArtifactFile(file, statNoFollow, tsgm);
+ FileValue oldFsValue = outputArtifactFileData.putIfAbsent(file, value);
+ checkInconsistentData(file, oldFsValue, null);
+ return value;
+ }
+
+ @VisibleForTesting
+ static FileValue fileValueFromArtifactFile(ArtifactFile file,
@Nullable FileStatusWithDigest statNoFollow, TimestampGranularityMonitor tsgm)
throws IOException {
- Path path = artifact.getPath();
+ Path path = file.getPath();
RootedPath rootedPath =
- RootedPath.toRootedPath(artifact.getRoot().getPath(), artifact.getRootRelativePath());
+ RootedPath.toRootedPath(file.getRoot().getPath(), file.getRootRelativePath());
if (statNoFollow == null) {
statNoFollow = FileStatusWithDigestAdapter.adapt(path.statIfFound(Symlinks.NOFOLLOW));
if (statNoFollow == null) {
@@ -371,7 +532,7 @@ public class ActionMetadataHandler implements MetadataHandler {
}
}
RootedPath realRootedPath = RootedPath.toRootedPathMaybeUnderRoot(realPath,
- ImmutableList.of(artifact.getRoot().getPath()));
+ ImmutableList.of(file.getRoot().getPath()));
FileStateValue fileStateValue;
FileStateValue realFileStateValue;
try {