Skip to content

Commit 2adab54

Browse files
committed
Lazily create local symlinks with BwoB
Unconditionally creating symlinks results in unnecessary I/O and may not even be supported by the host (e.g. on Windows). Instead, only create it in the remote action file system and rely on the prefetcher to materialize it when needed by downstream actions.
1 parent f4bf6a1 commit 2adab54

12 files changed

+388
-129
lines changed

src/main/java/com/google/devtools/build/lib/actions/FileArtifactValue.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ public boolean isRemote() {
164164
return false;
165165
}
166166

167+
/**
168+
* Returns whether the file contents are materialized lazily, for example, because they exist
169+
* remotely.
170+
*/
171+
public boolean isLazy() {
172+
return isRemote();
173+
}
174+
167175
/** Returns the location index for remote files. For non-remote files, returns 0. */
168176
public int getLocationIndex() {
169177
return 0;
@@ -311,7 +319,13 @@ public static FileArtifactValue createForSourceArtifact(
311319

312320
public static FileArtifactValue createFromInjectedDigest(
313321
FileArtifactValue metadata, @Nullable byte[] digest) {
314-
return createForNormalFile(digest, metadata.getContentsProxy(), metadata.getSize());
322+
var normalFileValue =
323+
createForNormalFile(digest, metadata.getContentsProxy(), metadata.getSize());
324+
if (metadata.getResolvedPath() != null) {
325+
return createFromExistingWithResolvedPath(normalFileValue, metadata.getResolvedPath());
326+
} else {
327+
return normalFileValue;
328+
}
315329
}
316330

317331
@VisibleForTesting
@@ -762,7 +776,6 @@ public void setContentsProxy(FileContentsProxy proxy) {
762776
this.proxy = proxy;
763777
}
764778

765-
766779
@Override
767780
public boolean equals(Object o) {
768781
if (this == o) {
@@ -890,6 +903,11 @@ public boolean isRemote() {
890903
return delegate.isRemote();
891904
}
892905

906+
@Override
907+
public boolean isLazy() {
908+
return true;
909+
}
910+
893911
@Override
894912
public int getLocationIndex() {
895913
return delegate.getLocationIndex();
@@ -1024,6 +1042,11 @@ public FileStateType getType() {
10241042
return FileStateType.SYMLINK;
10251043
}
10261044

1045+
@Override
1046+
public boolean isLazy() {
1047+
return true;
1048+
}
1049+
10271050
@Override
10281051
public byte[] getDigest() {
10291052
return digest;

src/main/java/com/google/devtools/build/lib/remote/AbstractActionInputPrefetcher.java

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
// limitations under the License.
1414
package com.google.devtools.build.lib.remote;
1515

16-
import static com.google.common.base.Preconditions.checkArgument;
1716
import static com.google.common.base.Preconditions.checkNotNull;
1817
import static com.google.common.base.Preconditions.checkState;
1918
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
@@ -41,6 +40,7 @@
4140
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
4241
import com.google.devtools.build.lib.actions.FileArtifactValue;
4342
import com.google.devtools.build.lib.actions.FileContentsProxy;
43+
import com.google.devtools.build.lib.actions.FileStateType;
4444
import com.google.devtools.build.lib.actions.InputMetadataProvider;
4545
import com.google.devtools.build.lib.actions.cache.OutputMetadataStore;
4646
import com.google.devtools.build.lib.actions.cache.VirtualActionInput;
@@ -196,15 +196,14 @@ void setOutputPermissions(Path dir) throws IOException {
196196
private final DirectoryTracker directoryTracker = new DirectoryTracker();
197197

198198
/** A symlink in the output tree. */
199-
record Symlink(PathFragment linkExecPath, PathFragment targetExecPath) {
199+
record Symlink(Path linkPath, Path targetPath) {
200200
Symlink {
201-
requireNonNull(linkExecPath, "linkExecPath");
202-
requireNonNull(targetExecPath, "targetExecPath");
203-
checkArgument(!linkExecPath.equals(targetExecPath));
201+
requireNonNull(linkPath, "linkPath");
202+
requireNonNull(targetPath, "targetPath");
204203
}
205204

206-
static Symlink of(PathFragment linkExecPath, PathFragment targetExecPath) {
207-
return new Symlink(linkExecPath, targetExecPath);
205+
static Symlink of(Path linkPath, Path targetPath) {
206+
return new Symlink(linkPath, targetPath);
208207
}
209208
}
210209

@@ -382,47 +381,56 @@ private ListenableFuture<Void> prefetchFile(
382381
return immediateVoidFuture();
383382
}
384383

385-
PathFragment execPath = input.getExecPath();
384+
Path path = execRoot.getRelative(input.getExecPath());
386385

387386
// Metadata may legitimately be missing, e.g. if this is an optional test output.
388387
FileArtifactValue metadata = metadataSupplier.getMetadata(input);
389-
if (metadata == null || !canDownloadFile(execRoot.getRelative(execPath), metadata)) {
388+
if (metadata == null || !metadata.isLazy()) {
390389
return immediateVoidFuture();
391390
}
392391

393392
@Nullable Symlink symlink = maybeGetSymlink(input, metadata, metadataSupplier);
394393

395394
if (symlink != null) {
396-
checkState(execPath.startsWith(symlink.linkExecPath()));
397-
execPath =
398-
symlink.targetExecPath().getRelative(execPath.relativeTo(symlink.linkExecPath()));
395+
checkState(path.startsWith(symlink.linkPath()));
396+
path = symlink.targetPath().getRelative(path.relativeTo(symlink.linkPath()));
399397
}
400398

401399
@Nullable PathFragment treeRootExecPath = maybeGetTreeRoot(input, metadataSupplier);
402400

403-
Completable result =
404-
downloadFileNoCheckRx(
405-
action,
406-
execRoot.getRelative(execPath),
407-
treeRootExecPath != null ? execRoot.getRelative(treeRootExecPath) : null,
408-
dirsWithOutputPermissions,
409-
input,
410-
metadata,
411-
priority,
412-
reason)
413-
.onErrorResumeNext(
414-
t -> {
415-
if (t instanceof CacheNotFoundException cacheNotFoundException) {
416-
// Only the symlink itself is guaranteed to be an input to the action, so
417-
// report its path for rewinding.
418-
cacheNotFoundException.setExecPath(input.getExecPath());
419-
return Completable.error(cacheNotFoundException);
420-
}
421-
return Completable.error(t);
422-
});
401+
Completable result;
402+
if (canDownloadFile(path, metadata)) {
403+
result =
404+
downloadFileNoCheckRx(
405+
action,
406+
path,
407+
treeRootExecPath != null ? execRoot.getRelative(treeRootExecPath) : null,
408+
dirsWithOutputPermissions,
409+
input,
410+
metadata,
411+
priority,
412+
reason)
413+
.onErrorResumeNext(
414+
t -> {
415+
if (t instanceof CacheNotFoundException cacheNotFoundException) {
416+
// Only the symlink itself is guaranteed to be an input to the action, so
417+
// report its path for rewinding.
418+
cacheNotFoundException.setExecPath(input.getExecPath());
419+
return Completable.error(cacheNotFoundException);
420+
}
421+
return Completable.error(t);
422+
});
423+
} else if (metadata.getType() == FileStateType.SYMLINK) {
424+
result = plantRelativeSymlink(path, metadata.getUnresolvedSymlinkTarget());
425+
} else {
426+
// This is a symlink to a local file.
427+
checkState(metadata.getResolvedPath() != null);
428+
checkState(!metadata.isRemote());
429+
result = Completable.complete();
430+
}
423431

424432
if (symlink != null) {
425-
result = result.andThen(plantSymlink(symlink));
433+
result = result.andThen(plantAbsoluteSymlink(symlink));
426434
}
427435

428436
return toListenableFuture(result);
@@ -474,9 +482,7 @@ private PathFragment maybeGetTreeRoot(ActionInput input, MetadataSupplier metada
474482
*/
475483
@Nullable
476484
private Symlink maybeGetSymlink(
477-
ActionInput input,
478-
FileArtifactValue metadata,
479-
MetadataSupplier metadataSupplier)
485+
ActionInput input, FileArtifactValue metadata, MetadataSupplier metadataSupplier)
480486
throws IOException, InterruptedException {
481487
if (input instanceof TreeFileArtifact treeFile) {
482488
SpecialArtifact treeArtifact = treeFile.getParent();
@@ -493,13 +499,13 @@ private Symlink maybeGetSymlink(
493499
}
494500
return maybeGetSymlink(treeArtifact, treeMetadata, metadataSupplier);
495501
}
496-
PathFragment execPath = input.getExecPath();
497-
PathFragment resolvedExecPath = execPath;
502+
Path path = execRoot.getRelative(input.getExecPath());
503+
Path resolvedPath = path;
498504
if (metadata.getResolvedPath() != null) {
499-
resolvedExecPath = metadata.getResolvedPath().relativeTo(execRoot.asFragment());
505+
resolvedPath = execRoot.getRelative(metadata.getResolvedPath());
500506
}
501-
if (!resolvedExecPath.equals(execPath)) {
502-
return Symlink.of(execPath, resolvedExecPath);
507+
if (!resolvedPath.equals(path)) {
508+
return Symlink.of(path, resolvedPath);
503509
}
504510
return null;
505511
}
@@ -683,17 +689,27 @@ private static void deletePartialDownload(Path path) {
683689
}
684690
}
685691

686-
private Completable plantSymlink(Symlink symlink) {
692+
private Completable plantRelativeSymlink(Path linkPath, String target) {
693+
return downloadCache.executeIfNot(
694+
linkPath,
695+
Completable.fromAction(
696+
() -> {
697+
// Delete the link path if it already exists. This is the case for tree artifacts,
698+
// whose root directory is created before the action runs.
699+
linkPath.delete();
700+
linkPath.createSymbolicLink(PathFragment.create(target));
701+
}));
702+
}
703+
704+
private Completable plantAbsoluteSymlink(Symlink symlink) {
687705
return downloadCache.executeIfNot(
688-
execRoot.getRelative(symlink.linkExecPath()),
706+
symlink.linkPath(),
689707
Completable.defer(
690708
() -> {
691-
Path link = execRoot.getRelative(symlink.linkExecPath());
692-
Path target = execRoot.getRelative(symlink.targetExecPath());
693709
// Delete the link path if it already exists. This is the case for tree artifacts,
694710
// whose root directory is created before the action runs.
695-
link.delete();
696-
link.createSymbolicLink(target);
711+
symlink.linkPath().delete();
712+
symlink.linkPath().createSymbolicLink(symlink.targetPath());
697713
return Completable.complete();
698714
}));
699715
}
@@ -732,7 +748,7 @@ public void finalizeAction(Action action, OutputMetadataStore outputMetadataStor
732748
}
733749

734750
var metadata = outputMetadataStore.getOutputMetadata(output);
735-
if (!metadata.isRemote()) {
751+
if (!metadata.isLazy()) {
736752
continue;
737753
}
738754

src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,17 @@ protected FileSystem getLocalFileSystem() {
283283

284284
/** Returns whether a path is stored remotely. Follows symlinks. */
285285
boolean isRemote(Path path) throws IOException {
286-
return isRemote(path.asFragment());
286+
// Files in the local filesystem are non-remote by definition, so stat only in-memory sources.
287+
var status = statInternal(path.asFragment(), FollowMode.FOLLOW_ALL, StatSources.IN_MEMORY_ONLY);
288+
return status instanceof FileStatusWithMetadata fileStatusWithMetadata
289+
&& fileStatusWithMetadata.getMetadata().isRemote();
287290
}
288291

289-
private boolean isRemote(PathFragment path) throws IOException {
290-
// Files in the local filesystem are non-remote by definition, so stat only in-memory sources.
292+
private boolean isLazy(PathFragment path) throws IOException {
293+
// Files in the local filesystem are non-lazy by definition, so stat only in-memory sources.
291294
var status = statInternal(path, FollowMode.FOLLOW_ALL, StatSources.IN_MEMORY_ONLY);
292295
return status instanceof FileStatusWithMetadata fileStatusWithMetadata
293-
&& fileStatusWithMetadata.getMetadata().isRemote();
296+
&& fileStatusWithMetadata.getMetadata().isLazy();
294297
}
295298

296299
public void updateContext(ActionExecutionMetadata action) {
@@ -368,7 +371,7 @@ protected boolean delete(PathFragment path) throws IOException {
368371
@Override
369372
protected InputStream getInputStream(PathFragment path) throws IOException {
370373
try {
371-
downloadIfRemote(path);
374+
materializeIfLazy(path);
372375
} catch (BulkTransferException e) {
373376
var newlyLostInputs = e.getLostArtifacts(inputArtifactData::getInput);
374377
if (!newlyLostInputs.isEmpty()) {
@@ -379,8 +382,8 @@ protected InputStream getInputStream(PathFragment path) throws IOException {
379382
return localFs.getPath(path).getInputStream();
380383
}
381384

382-
private void downloadIfRemote(PathFragment path) throws IOException {
383-
if (!isRemote(path)) {
385+
private void materializeIfLazy(PathFragment path) throws IOException {
386+
if (!isLazy(path)) {
384387
return;
385388
}
386389
PathFragment execPath = path.relativeTo(execRoot);
@@ -592,9 +595,9 @@ protected void createSymbolicLink(PathFragment linkPath, PathFragment targetFrag
592595

593596
if (isOutput(linkPath)) {
594597
remoteOutputTree.getPath(linkPath).createSymbolicLink(targetFragment);
598+
} else {
599+
localFs.getPath(linkPath).createSymbolicLink(targetFragment);
595600
}
596-
597-
localFs.getPath(linkPath).createSymbolicLink(targetFragment);
598601
}
599602

600603
@Override

src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,26 +1005,36 @@ private void moveOutputsToFinalLocation(
10051005
}
10061006
}
10071007

1008-
private void createSymlinks(Iterable<SymlinkMetadata> symlinks) throws IOException {
1008+
private void createSymlinks(
1009+
RemoteActionResult result,
1010+
@Nullable RemoteActionFileSystem remoteActionFileSystem,
1011+
Iterable<SymlinkMetadata> symlinks,
1012+
@Nullable PathFragment treeRootExecPath)
1013+
throws IOException {
10091014
for (SymlinkMetadata symlink : symlinks) {
1010-
Preconditions.checkNotNull(
1011-
symlink.path().getParentDirectory(),
1012-
"Failed creating directory and parents for %s",
1013-
symlink.path())
1014-
.createDirectoryAndParents();
1015-
// If a directory output is being materialized as a symlink, creating the symlink fails as we
1016-
// must first delete the preexisting empty directory. Since this is rare (and in the future
1017-
// BwoB may no longer eagerly create these directories), we don't delete the directory
1018-
// beforehand.
1019-
try {
1020-
symlink.path().createSymbolicLink(symlink.target());
1021-
} catch (IOException e) {
1022-
if (!symlink.path().isDirectory(Symlinks.NOFOLLOW)) {
1023-
throw e;
1015+
if (shouldDownload(result, symlink.path().relativeTo(execRoot), treeRootExecPath)) {
1016+
Preconditions.checkNotNull(
1017+
symlink.path().getParentDirectory(),
1018+
"Failed creating directory and parents for %s",
1019+
symlink.path())
1020+
.createDirectoryAndParents();
1021+
// If a directory output is being materialized as a symlink, creating the symlink fails as
1022+
// we must first delete the preexisting empty directory. Since this is rare (and in the
1023+
// future BwoB may no longer eagerly create these directories), we don't delete the
1024+
// directory beforehand.
1025+
try {
1026+
symlink.path().createSymbolicLink(symlink.target());
1027+
} catch (IOException e) {
1028+
if (!symlink.path().isDirectory(Symlinks.NOFOLLOW)) {
1029+
throw e;
1030+
}
1031+
// Retry after deleting the directory.
1032+
symlink.path().delete();
1033+
symlink.path().createSymbolicLink(symlink.target());
10241034
}
1025-
// Retry after deleting the directory.
1026-
symlink.path().delete();
1027-
symlink.path().createSymbolicLink(symlink.target());
1035+
} else if (!(outputService instanceof BazelOutputService)) {
1036+
checkNotNull(remoteActionFileSystem)
1037+
.createSymbolicLink(symlink.path().asFragment(), symlink.target());
10281038
}
10291039
}
10301040
}
@@ -1461,19 +1471,19 @@ public InMemoryOutput downloadOutputs(RemoteAction action, RemoteActionResult re
14611471
Iterables.transform(finishedDownloads, FileMetadata::path), realToTmpPath);
14621472
}
14631473

1464-
List<SymlinkMetadata> symlinksInDirectories = new ArrayList<>();
1465-
for (Entry<Path, DirectoryMetadata> entry : metadata.directories()) {
1466-
for (SymlinkMetadata symlink : entry.getValue().symlinks()) {
1467-
symlinksInDirectories.add(symlink);
1468-
}
1469-
}
1470-
1471-
Iterable<SymlinkMetadata> symlinks =
1472-
Iterables.concat(metadata.symlinks(), symlinksInDirectories);
1473-
14741474
// Create the symbolic links after all downloads are finished, because dangling symlinks
14751475
// might not be supported on all platforms.
1476-
createSymlinks(symlinks);
1476+
for (var symlink : metadata.symlinks()) {
1477+
createSymlinks(
1478+
result, remoteActionFileSystem, ImmutableList.of(symlink), /* treeRootExecPath= */ null);
1479+
}
1480+
for (Entry<Path, DirectoryMetadata> entry : metadata.directories()) {
1481+
createSymlinks(
1482+
result,
1483+
remoteActionFileSystem,
1484+
entry.getValue().symlinks(),
1485+
entry.getKey().relativeTo(execRoot));
1486+
}
14771487

14781488
if (result.success()) {
14791489
// Check that all mandatory outputs are created.

0 commit comments

Comments
 (0)