Skip to content

Commit 46c7dcf

Browse files
iancha1992sluongng
andauthored
RemoteExecutionService: support output_symlinks in ActionResult (#18441)
Since Remote API v2.1, both - output_file_symlinks - output_directory_symlinks were marked as deprecated in favor of output_symlinks. However, Bazel downloadOutputs has only support the 2 deprecated fields and not output_symlinks. Add support for output_symlinks. Closes #18198. PiperOrigin-RevId: 527511382 Change-Id: I350ab77a5d30ab06dab4118cf76f0b0e7650a739 Co-authored-by: Son Luong Ngoc <[email protected]>
1 parent 828ae6f commit 46c7dcf

File tree

2 files changed

+106
-11
lines changed

2 files changed

+106
-11
lines changed

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public RemoteExecutionService(
210210
this.tempPathGenerator = tempPathGenerator;
211211
this.captureCorruptedOutputsDir = captureCorruptedOutputsDir;
212212

213-
this.scheduler = Schedulers.from(executor, /*interruptibleWorker=*/ true);
213+
this.scheduler = Schedulers.from(executor, /* interruptibleWorker= */ true);
214214
}
215215

216216
static Command buildCommand(
@@ -639,6 +639,10 @@ public List<OutputSymlink> getOutputDirectorySymlinks() {
639639
return actionResult.getOutputDirectorySymlinksList();
640640
}
641641

642+
public List<OutputSymlink> getOutputSymlinks() {
643+
return actionResult.getOutputSymlinksList();
644+
}
645+
642646
/**
643647
* Returns the freeform informational message with details on the execution of the action that
644648
* may be displayed to the user upon failure or when requested explicitly.
@@ -1005,7 +1009,7 @@ ActionResultMetadata parseActionResultMetadata(
10051009
directExecutor()));
10061010
}
10071011

1008-
waitForBulkTransfer(dirMetadataDownloads.values(), /* cancelRemainingOnInterrupt=*/ true);
1012+
waitForBulkTransfer(dirMetadataDownloads.values(), /* cancelRemainingOnInterrupt= */ true);
10091013

10101014
ImmutableMap.Builder<Path, DirectoryMetadata> directories = ImmutableMap.builder();
10111015
for (Map.Entry<Path, ListenableFuture<Tree>> metadataDownload :
@@ -1029,18 +1033,33 @@ ActionResultMetadata parseActionResultMetadata(
10291033
new FileMetadata(localPath, outputFile.getDigest(), outputFile.getIsExecutable()));
10301034
}
10311035

1032-
ImmutableMap.Builder<Path, SymlinkMetadata> symlinks = ImmutableMap.builder();
1033-
Iterable<OutputSymlink> outputSymlinks =
1034-
Iterables.concat(result.getOutputFileSymlinks(), result.getOutputDirectorySymlinks());
1035-
for (OutputSymlink symlink : outputSymlinks) {
1036-
Path localPath =
1036+
var symlinkMap = new HashMap<Path, SymlinkMetadata>();
1037+
var outputSymlinks =
1038+
Iterables.concat(
1039+
result.getOutputFileSymlinks(),
1040+
result.getOutputDirectorySymlinks(),
1041+
result.getOutputSymlinks());
1042+
for (var symlink : outputSymlinks) {
1043+
var localPath =
10371044
remotePathResolver.outputPathToLocalPath(encodeBytestringUtf8(symlink.getPath()));
1038-
symlinks.put(
1039-
localPath, new SymlinkMetadata(localPath, PathFragment.create(symlink.getTarget())));
1045+
var target = PathFragment.create(symlink.getTarget());
1046+
var existingMetadata = symlinkMap.get(localPath);
1047+
if (existingMetadata != null) {
1048+
if (!target.equals(existingMetadata.target())) {
1049+
throw new IOException(
1050+
String.format(
1051+
"Symlink path collision: '%s' is mapped to both '%s' and '%s'. Action Result"
1052+
+ " should not contain multiple targets for the same symlink.",
1053+
localPath, existingMetadata.target(), target));
1054+
}
1055+
continue;
1056+
}
1057+
1058+
symlinkMap.put(localPath, new SymlinkMetadata(localPath, target));
10401059
}
10411060

10421061
return new ActionResultMetadata(
1043-
files.buildOrThrow(), symlinks.buildOrThrow(), directories.buildOrThrow());
1062+
files.buildOrThrow(), ImmutableMap.copyOf(symlinkMap), directories.buildOrThrow());
10441063
}
10451064

10461065
/**
@@ -1156,7 +1175,7 @@ public InMemoryOutput downloadOutputs(RemoteAction action, RemoteActionResult re
11561175
ListenableFuture<byte[]> inMemoryOutputDownload =
11571176
remoteCache.downloadBlob(context, inMemoryOutputDigest);
11581177
waitForBulkTransfer(
1159-
ImmutableList.of(inMemoryOutputDownload), /* cancelRemainingOnInterrupt=*/ true);
1178+
ImmutableList.of(inMemoryOutputDownload), /* cancelRemainingOnInterrupt= */ true);
11601179
byte[] data = getFromFuture(inMemoryOutputDownload);
11611180
return new InMemoryOutput(inMemoryOutput, ByteString.copyFrom(data));
11621181
}

src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,54 @@ public void downloadOutputs_relativeDirectorySymlink_success() throws Exception
575575
assertThat(context.isLockOutputFilesCalled()).isTrue();
576576
}
577577

578+
@Test
579+
public void downloadOutputs_relativeOutputSymlinks_success() throws Exception {
580+
// Test that download outputs works when the action result only contains output_symlinks
581+
// and not output_file_symlinks or output_directory_symlinks, which are deprecated.
582+
ActionResult.Builder builder = ActionResult.newBuilder();
583+
builder.addOutputSymlinksBuilder().setPath("outputs/a/b/link").setTarget("../../foo");
584+
RemoteActionResult result =
585+
RemoteActionResult.createFromCache(CachedActionResult.remote(builder.build()));
586+
Spawn spawn = newSpawnFromResult(result);
587+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
588+
RemoteExecutionService service = newRemoteExecutionService();
589+
RemoteAction action = service.buildRemoteAction(spawn, context);
590+
591+
// Doesn't check for dangling links, hence download succeeds.
592+
service.downloadOutputs(action, result);
593+
594+
Path path = execRoot.getRelative("outputs/a/b/link");
595+
assertThat(path.isSymbolicLink()).isTrue();
596+
assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("../../foo"));
597+
assertThat(context.isLockOutputFilesCalled()).isTrue();
598+
}
599+
600+
@Test
601+
public void downloadOutputs_outputSymlinksCompatibility_success() throws Exception {
602+
// Test that download outputs works when the action result contains both output_symlinks
603+
// and output_file_symlinks (or output_directory_symlinks).
604+
//
605+
// Remote Execution Server may set both fields to ensure backward compatibility with
606+
// clients that don't support output_symlinks.
607+
ActionResult.Builder builder = ActionResult.newBuilder();
608+
builder.addOutputFileSymlinksBuilder().setPath("outputs/a/b/link").setTarget("foo");
609+
builder.addOutputSymlinksBuilder().setPath("outputs/a/b/link").setTarget("foo");
610+
RemoteActionResult result =
611+
RemoteActionResult.createFromCache(CachedActionResult.remote(builder.build()));
612+
Spawn spawn = newSpawnFromResult(result);
613+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
614+
RemoteExecutionService service = newRemoteExecutionService();
615+
RemoteAction action = service.buildRemoteAction(spawn, context);
616+
617+
// Doesn't check for dangling links, hence download succeeds.
618+
service.downloadOutputs(action, result);
619+
620+
Path path = execRoot.getRelative("outputs/a/b/link");
621+
assertThat(path.isSymbolicLink()).isTrue();
622+
assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("foo"));
623+
assertThat(context.isLockOutputFilesCalled()).isTrue();
624+
}
625+
578626
@Test
579627
public void downloadOutputs_relativeSymlinkInDirectory_success() throws Exception {
580628
Tree tree =
@@ -666,6 +714,28 @@ public void downloadOutputs_absoluteSymlinkInDirectory_error() throws Exception
666714
assertThat(context.isLockOutputFilesCalled()).isTrue();
667715
}
668716

717+
@Test
718+
public void downloadOutputs_symlinkCollision_error() throws Exception {
719+
ActionResult.Builder builder = ActionResult.newBuilder();
720+
builder.addOutputDirectorySymlinksBuilder().setPath("outputs/foo").setTarget("foo1");
721+
builder.addOutputSymlinksBuilder().setPath("outputs/foo").setTarget("foo2");
722+
RemoteActionResult result =
723+
RemoteActionResult.createFromCache(CachedActionResult.remote(builder.build()));
724+
Spawn spawn = newSpawnFromResult(result);
725+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
726+
RemoteExecutionService service = newRemoteExecutionService();
727+
RemoteAction action = service.buildRemoteAction(spawn, context);
728+
729+
IOException expected =
730+
assertThrows(IOException.class, () -> service.downloadOutputs(action, result));
731+
732+
assertThat(expected.getSuppressed()).isEmpty();
733+
assertThat(expected).hasMessageThat().contains("Symlink path collision");
734+
assertThat(expected).hasMessageThat().contains("outputs/foo");
735+
assertThat(expected).hasMessageThat().contains("foo1");
736+
assertThat(expected).hasMessageThat().contains("foo2");
737+
}
738+
669739
@Test
670740
public void downloadOutputs_onFailure_maintainDirectories() throws Exception {
671741
// Test that output directories are not deleted on download failure. See
@@ -1948,6 +2018,12 @@ private Spawn newSpawnFromResult(
19482018
outputs.add(output);
19492019
}
19502020

2021+
for (OutputSymlink symlink : result.getOutputSymlinks()) {
2022+
Path path = remotePathResolver.outputPathToLocalPath(symlink.getPath());
2023+
Artifact output = ActionsTestUtil.createArtifact(artifactRoot, path);
2024+
outputs.add(output);
2025+
}
2026+
19512027
return newSpawn(executionInfo, outputs.build());
19522028
}
19532029

0 commit comments

Comments
 (0)