aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar ulfjack <ulfjack@google.com>2018-01-19 02:55:21 -0800
committerGravatar Copybara-Service <copybara-piper@google.com>2018-01-19 02:56:58 -0800
commit8896d2e211c57453359566c4520615c1c1f75b66 (patch)
tree05b8d52d6a254800377f1e9d479acae5584a348a
parent76eb2a4ef5747ef881f4a3d2d1e3338e4ef8913f (diff)
Prevent broken cache entries on concurrent file changes
Local execution has an inherent race condition: if a user modifies a file while an action is executed, then it is impossible for Bazel to tell which version of the file was actually read during action execution. The file may have been modified before or after the tool has read it, or, in the worst case, the tool may have read both the original and the modified version. In addition, the file may be changed back to the original state before Bazel can check the file, so computing the digest before / after may not be sufficient. This is a concern for both local and remote caches, although the cost of poisoning a shared remote cache is significantly higher, and is what has triggered this work. Fixes #3360. We solve this by keeping a reference to the FileContentsProxy, and using that to check for modificaitons before storing the cache entry. We output a warning if this check fails. This change does not increase memory consumption; Java objects are always allocated in multiples of 8 bytes, we use compressed oops, and the FileArtifactValue currently has 12 bytes worth of fields (excl. object overhead), so adding another pointer is effectively free. As a possible performance optimization on purely local builds, we could also consider not computing digests at all, and only use the FileContentsProxy for caching. PiperOrigin-RevId: 182510358
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java24
-rw-r--r--src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java12
-rw-r--r--src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java23
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java2
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java104
-rw-r--r--src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java8
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/FileArtifactValueTest.java63
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java4
8 files changed, 192 insertions, 48 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java b/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java
index c393332fe3..76479d0377 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SingleBuildFileCache.java
@@ -68,24 +68,20 @@ public class SingleBuildFileCache implements ActionInputFileCache {
? ((Artifact) input).getPath()
: execRoot.getRelative(input.getExecPath());
try {
- byte[] digest = path.getDigest();
+ FileArtifactValue metadata = FileArtifactValue.create(path);
+ if (metadata.getType().isDirectory()) {
+ throw new DigestOfDirectoryException(
+ "Input is a directory: " + input.getExecPathString());
+ }
BaseEncoding hex = BaseEncoding.base16().lowerCase();
ByteString hexDigest =
- ByteString.copyFrom(hex.encode(digest).getBytes(US_ASCII));
+ ByteString.copyFrom(hex.encode(metadata.getDigest()).getBytes(US_ASCII));
// Inject reverse mapping. Doing this unconditionally in getDigest() showed up
// as a hotspot in CPU profiling.
digestToPath.put(hexDigest, input);
- return new ActionInputMetadata(digest, path.getFileSize());
+ return new ActionInputMetadata(metadata);
} catch (IOException e) {
- if (path.isDirectory()) {
- // TODO(bazel-team): This is rather presumptuous- it could have been another
- // type of IOException.
- return new ActionInputMetadata(
- new DigestOfDirectoryException(
- "Input is a directory: " + input.getExecPathString()));
- } else {
- return new ActionInputMetadata(e);
- }
+ return new ActionInputMetadata(e);
}
}
});
@@ -119,8 +115,8 @@ public class SingleBuildFileCache implements ActionInputFileCache {
private final IOException exceptionOnAccess;
/** Constructor for a successful lookup. */
- ActionInputMetadata(byte[] digest, long size) {
- this.metadata = FileArtifactValue.createNormalFile(digest, size);
+ ActionInputMetadata(Metadata metadata) {
+ this.metadata = metadata;
this.exceptionOnAccess = null;
}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java
index b9b1f55d79..21ab0a5d0b 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteOptions.java
@@ -211,4 +211,16 @@ public final class RemoteOptions extends OptionsBase {
help = "A file path to a local disk cache."
)
public PathFragment experimentalLocalDiskCachePath;
+
+ @Option(
+ name = "experimental_guard_against_concurrent_changes",
+ defaultValue = "true",
+ category = "remote",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Turn this off to disable checking the ctime of input files of an action before "
+ + "uploading it to a remote cache. There may be cases where the Linux kernel delays "
+ + "writing of files, which could cause false positives."
+ )
+ public boolean experimentalGuardAgainstConcurrentChanges;
}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
index e8af197907..04977ed157 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
@@ -20,6 +20,7 @@ import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.Spawns;
+import com.google.devtools.build.lib.actions.cache.Metadata;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
@@ -27,6 +28,7 @@ import com.google.devtools.build.lib.exec.SpawnCache;
import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy;
import com.google.devtools.build.lib.remote.DigestUtil.ActionKey;
import com.google.devtools.build.lib.remote.TreeNodeRepository.TreeNode;
+import com.google.devtools.build.lib.skyframe.FileArtifactValue;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.remoteexecution.v1test.Action;
@@ -157,6 +159,14 @@ final class RemoteSpawnCache implements SpawnCache {
@Override
public void store(SpawnResult result, Collection<Path> files)
throws InterruptedException, IOException {
+ if (options.experimentalGuardAgainstConcurrentChanges) {
+ try {
+ checkForConcurrentModifications();
+ } catch (IOException e) {
+ report(Event.warn(e.getMessage()));
+ return;
+ }
+ }
boolean uploadAction =
Spawns.mayBeCached(spawn)
&& Status.SUCCESS.equals(result.status())
@@ -179,6 +189,19 @@ final class RemoteSpawnCache implements SpawnCache {
@Override
public void close() {}
+
+ private void checkForConcurrentModifications() throws IOException {
+ for (ActionInput input : inputMap.values()) {
+ Metadata metadata = policy.getActionInputFileCache().getMetadata(input);
+ if (metadata instanceof FileArtifactValue) {
+ FileArtifactValue artifactValue = (FileArtifactValue) metadata;
+ Path path = execRoot.getRelative(input.getExecPath());
+ if (artifactValue.wasModifiedSinceDigest(path)) {
+ throw new IOException(path + " was modified during execution");
+ }
+ }
+ }
+ }
};
} else {
return SpawnCache.NO_RESULT_NO_STORE;
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
index 627184df44..2fe2b69ef2 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
@@ -268,7 +268,7 @@ class ArtifactFunction implements SkyFunction {
// Directories are special-cased because their mtimes are used, so should have been constructed
// during execution of the action (in ActionMetadataHandler#maybeStoreAdditionalData).
Preconditions.checkState(data.isFile(), "Unexpected not file %s (%s)", artifact, data);
- return FileArtifactValue.createNormalFile(data.getDigest(), data.getSize());
+ return FileArtifactValue.createNormalFile(data);
}
private static AggregatingArtifactValue createAggregatingValue(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
index 06b2aefcc9..aaba2245dc 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
@@ -24,6 +24,7 @@ import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.util.Arrays;
@@ -68,6 +69,11 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
}
@Override
+ public boolean wasModifiedSinceDigest(Path path) throws IOException {
+ return false;
+ }
+
+ @Override
public String toString() {
return "singleton marker artifact value (" + hashCode() + ")";
}
@@ -95,6 +101,11 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
}
@Override
+ public boolean wasModifiedSinceDigest(Path path) throws IOException {
+ return false;
+ }
+
+ @Override
public String toString() {
return "OMITTED_FILE_MARKER";
}
@@ -140,6 +151,11 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
}
@Override
+ public boolean wasModifiedSinceDigest(Path path) throws IOException {
+ return false;
+ }
+
+ @Override
public String toString() {
return MoreObjects.toStringHelper(this).add("mtime", mtime).toString();
}
@@ -147,10 +163,12 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
private static final class RegularFileArtifactValue extends FileArtifactValue {
private final byte[] digest;
+ @Nullable private final FileContentsProxy proxy;
private final long size;
- private RegularFileArtifactValue(byte[] digest, long size) {
+ private RegularFileArtifactValue(byte[] digest, @Nullable FileContentsProxy proxy, long size) {
this.digest = Preconditions.checkNotNull(digest);
+ this.proxy = proxy;
this.size = size;
}
@@ -170,6 +188,15 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
}
@Override
+ public boolean wasModifiedSinceDigest(Path path) throws IOException {
+ if (proxy == null) {
+ return false;
+ }
+ FileStatus stat = path.statIfFound(Symlinks.FOLLOW);
+ return stat == null || !stat.isFile() || !proxy.equals(FileContentsProxy.create(stat));
+ }
+
+ @Override
public long getModifiedTime() {
throw new UnsupportedOperationException(
"regular file's mtime should never be called. (" + this + ")");
@@ -181,51 +208,77 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
}
}
- @VisibleForTesting
- public static FileArtifactValue create(Artifact artifact) throws IOException {
- return create(artifact.getPath());
- }
-
static FileArtifactValue create(Artifact artifact, FileValue fileValue) throws IOException {
boolean isFile = fileValue.isFile();
- return create(artifact.getPath(), isFile, isFile ? fileValue.getSize() : 0,
+ FileContentsProxy proxy = getProxyFromFileStateValue(fileValue.realFileStateValue());
+ return create(artifact.getPath(), isFile, isFile ? fileValue.getSize() : 0, proxy,
isFile ? fileValue.getDigest() : null);
}
static FileArtifactValue create(
Artifact artifact, FileValue fileValue, @Nullable byte[] injectedDigest) throws IOException {
boolean isFile = fileValue.isFile();
- return create(artifact.getPath(), isFile, isFile ? fileValue.getSize() : 0, injectedDigest);
+ FileContentsProxy proxy = getProxyFromFileStateValue(fileValue.realFileStateValue());
+ return create(artifact.getPath(), isFile, isFile ? fileValue.getSize() : 0, proxy,
+ injectedDigest);
+ }
+
+ @VisibleForTesting
+ public static FileArtifactValue create(Artifact artifact) throws IOException {
+ return create(artifact.getPath());
}
@VisibleForTesting
- static FileArtifactValue create(Path path) throws IOException {
- FileStatus stat = path.stat();
- boolean isFile = stat.isFile();
- return create(path, isFile, isFile ? stat.getSize() : 0, null);
+ public static FileArtifactValue create(Path path) throws IOException {
+ // Caution: there's a race condition between stating the file and computing the
+ // digest. We need to stat first, since we're using the stat to detect changes.
+ // We follow symlinks here to be consistent with getDigest.
+ FileStatus stat = path.stat(Symlinks.FOLLOW);
+ return create(path, stat.isFile(), stat.getSize(), FileContentsProxy.create(stat), null);
}
private static FileArtifactValue create(
- Path path, boolean isFile, long size, @Nullable byte[] digest) throws IOException {
- if (isFile && digest == null) {
- digest = DigestUtils.getDigestOrFail(path, size);
- }
+ Path path, boolean isFile, long size, FileContentsProxy proxy, @Nullable byte[] digest)
+ throws IOException {
if (!isFile) {
// In this case, we need to store the mtime because the action cache uses mtime for
// directories to determine if this artifact has changed. We want this code path to go away
// somehow (maybe by implementing FileSet in Skyframe).
return new DirectoryArtifactValue(path.getLastModifiedTime());
}
+ if (digest == null) {
+ digest = DigestUtils.getDigestOrFail(path, size);
+ }
Preconditions.checkState(digest != null, path);
- return createNormalFile(digest, size);
+ return new RegularFileArtifactValue(digest, proxy, size);
}
- public static FileArtifactValue createNormalFile(byte[] digest, long size) {
- return new RegularFileArtifactValue(digest, size);
+ public static FileArtifactValue createForVirtualActionInput(byte[] digest, long size) {
+ return new RegularFileArtifactValue(digest, /*proxy=*/ null, size);
+ }
+
+ public static FileArtifactValue createNormalFile(
+ byte[] digest, @Nullable FileContentsProxy proxy, long size) {
+ return new RegularFileArtifactValue(digest, proxy, size);
}
static FileArtifactValue createNormalFile(FileValue fileValue) {
- return new RegularFileArtifactValue(fileValue.getDigest(), fileValue.getSize());
+ FileContentsProxy proxy = getProxyFromFileStateValue(fileValue.realFileStateValue());
+ return new RegularFileArtifactValue(fileValue.getDigest(), proxy, fileValue.getSize());
+ }
+
+ private static FileContentsProxy getProxyFromFileStateValue(FileStateValue value) {
+ if (value instanceof FileStateValue.RegularFileStateValue) {
+ return ((FileStateValue.RegularFileStateValue) value).getContentsProxy();
+ } else if (value instanceof FileStateValue.SpecialFileStateValue) {
+ return ((FileStateValue.SpecialFileStateValue) value).getContentsProxy();
+ }
+ return null;
+ }
+
+ @VisibleForTesting
+ public static FileArtifactValue createNormalFile(byte[] digest, long size) {
+ return createNormalFile(digest, /*proxy=*/null, size);
}
public static FileArtifactValue createDirectory(long mtime) {
@@ -238,7 +291,7 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
*/
static FileArtifactValue createProxy(byte[] digest) {
Preconditions.checkNotNull(digest);
- return createNormalFile(digest, /*size=*/ 0);
+ return createNormalFile(digest, /*proxy=*/ null, /*size=*/ 0);
}
@Override
@@ -254,6 +307,15 @@ public abstract class FileArtifactValue implements SkyValue, Metadata {
@Override
public abstract long getModifiedTime();
+ /**
+ * Provides a best-effort determination whether the file was changed since the digest was
+ * computed. This method performs file system I/O, so may be expensive. It's primarily intended to
+ * avoid storing bad cache entries in an action cache. It should return true if there is a chance
+ * that the file was modified since the digest was computed. Better not upload if we are not sure
+ * that the cache entry is reliable.
+ */
+ public abstract boolean wasModifiedSinceDigest(Path path) throws IOException;
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java b/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java
index dd37b3cfbd..c4c09ab068 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java
@@ -21,8 +21,11 @@ import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputFileCache;
import com.google.devtools.build.lib.actions.cache.Metadata;
import com.google.devtools.build.lib.skyframe.FileArtifactValue;
+import com.google.devtools.build.lib.skyframe.FileContentsProxy;
+import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.remoteexecution.v1test.Digest;
import com.google.devtools.remoteexecution.v1test.Tree;
import com.google.protobuf.ByteString;
@@ -43,9 +46,10 @@ final class FakeActionInputFileCache implements ActionInputFileCache {
@Override
public Metadata getMetadata(ActionInput input) throws IOException {
String hexDigest = Preconditions.checkNotNull(cas.get(input), input);
+ Path path = execRoot.getRelative(input.getExecPath());
+ FileStatus stat = path.stat(Symlinks.FOLLOW);
return FileArtifactValue.createNormalFile(
- HashCode.fromString(hexDigest).asBytes(),
- execRoot.getRelative(input.getExecPath()).getFileSize());
+ HashCode.fromString(hexDigest).asBytes(), FileContentsProxy.create(stat), stat.getSize());
}
void setDigest(ActionInput input, String digest) {
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FileArtifactValueTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FileArtifactValueTest.java
index ccf282243f..7637abd3ba 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FileArtifactValueTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FileArtifactValueTest.java
@@ -19,6 +19,7 @@ import static org.junit.Assert.fail;
import com.google.common.io.BaseEncoding;
import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.testutil.ManualClock;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
@@ -30,11 +31,12 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class FileArtifactValueTest {
- private final FileSystem fs = new InMemoryFileSystem();
+ private final ManualClock clock = new ManualClock();
+ private final FileSystem fs = new InMemoryFileSystem(clock);
private Path scratchFile(String name, long mtime, String content) throws IOException {
Path path = fs.getPath(name);
- FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+ path.getParentDirectory().createDirectoryAndParents();
FileSystemUtils.writeContentAsLatin1(path, content);
path.setLastModifiedTime(mtime);
return path;
@@ -42,7 +44,7 @@ public class FileArtifactValueTest {
private Path scratchDir(String name, long mtime) throws IOException {
Path path = fs.getPath(name);
- FileSystemUtils.createDirectoryAndParents(path);
+ path.createDirectoryAndParents();
path.setLastModifiedTime(mtime);
return path;
}
@@ -114,9 +116,7 @@ public class FileArtifactValueTest {
@Test
public void testDirectory() throws Exception {
- Path path = fs.getPath("/dir");
- FileSystemUtils.createDirectoryAndParents(path);
- path.setLastModifiedTime(1L);
+ Path path = scratchDir("/dir", /*mtime=*/ 1L);
FileArtifactValue value = create(path);
assertThat(value.getDigest()).isNull();
assertThat(value.getModifiedTime()).isEqualTo(1L);
@@ -153,7 +153,7 @@ public class FileArtifactValueTest {
}
};
Path path = fs.getPath("/some/path");
- FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
+ path.getParentDirectory().createDirectoryAndParents();
FileSystemUtils.writeContentAsLatin1(path, "content");
try {
create(path);
@@ -162,4 +162,53 @@ public class FileArtifactValueTest {
assertThat(e).isSameAs(exception);
}
}
+
+ @Test
+ public void testUptodateCheck() throws Exception {
+ Path path = scratchFile("/dir/artifact1", 0L, "content");
+ FileArtifactValue value = create(path);
+ clock.advanceMillis(1);
+ assertThat(value.wasModifiedSinceDigest(path)).isFalse();
+ clock.advanceMillis(1);
+ assertThat(value.wasModifiedSinceDigest(path)).isFalse();
+ clock.advanceMillis(1);
+ path.setLastModifiedTime(123); // Changing mtime implicitly updates ctime.
+ assertThat(value.wasModifiedSinceDigest(path)).isTrue();
+ clock.advanceMillis(1);
+ assertThat(value.wasModifiedSinceDigest(path)).isTrue();
+ }
+
+ @Test
+ public void testUptodateCheckDeleteFile() throws Exception {
+ Path path = scratchFile("/dir/artifact1", 0L, "content");
+ FileArtifactValue value = create(path);
+ assertThat(value.wasModifiedSinceDigest(path)).isFalse();
+ path.delete();
+ assertThat(value.wasModifiedSinceDigest(path)).isTrue();
+ }
+
+ @Test
+ public void testUptodateCheckDirectory() throws Exception {
+ // For now, we don't attempt to detect changes to directories.
+ Path path = scratchDir("/dir", 0L);
+ FileArtifactValue value = create(path);
+ assertThat(value.wasModifiedSinceDigest(path)).isFalse();
+ path.delete();
+ clock.advanceMillis(1);
+ assertThat(value.wasModifiedSinceDigest(path)).isFalse();
+ }
+
+ @Test
+ public void testUptodateChangeFileToDirectory() throws Exception {
+ // For now, we don't attempt to detect changes to directories.
+ Path path = scratchFile("/dir/file", 0L, "");
+ FileArtifactValue value = create(path);
+ assertThat(value.wasModifiedSinceDigest(path)).isFalse();
+ // If we only check ctime, then we need to change the clock here, or we get a ctime match on the
+ // stat.
+ path.delete();
+ path.createDirectoryAndParents();
+ clock.advanceMillis(1);
+ assertThat(value.wasModifiedSinceDigest(path)).isTrue();
+ }
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java
index 3c32a9df3d..4e3b242b5d 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java
@@ -102,9 +102,7 @@ public class TreeArtifactMetadataTest extends ArtifactFunctionTestCase {
// breaking changes.
Map<String, Metadata> digestBuilder = new HashMap<>();
for (PathFragment child : children) {
- Metadata subdigest = FileArtifactValue.createNormalFile(
- tree.getPath().getRelative(child).getDigest(),
- tree.getPath().getRelative(child).getFileSize());
+ Metadata subdigest = FileArtifactValue.create(tree.getPath().getRelative(child));
digestBuilder.put(child.getPathString(), subdigest);
}
assertThat(DigestUtils.fromMetadata(digestBuilder).getDigestBytesUnsafe())