Skip to content

Commit e842fd5

Browse files
sluongngcopybara-github
authored andcommitted
RemoteExecutionService: support output_symlinks in ActionResult
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
1 parent 72212f2 commit e842fd5

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
@@ -213,7 +213,7 @@ public RemoteExecutionService(
213213
this.tempPathGenerator = tempPathGenerator;
214214
this.captureCorruptedOutputsDir = captureCorruptedOutputsDir;
215215

216-
this.scheduler = Schedulers.from(executor, /*interruptibleWorker=*/ true);
216+
this.scheduler = Schedulers.from(executor, /* interruptibleWorker= */ true);
217217
}
218218

219219
static Command buildCommand(
@@ -644,6 +644,10 @@ public List<OutputSymlink> getOutputDirectorySymlinks() {
644644
return actionResult.getOutputDirectorySymlinksList();
645645
}
646646

647+
public List<OutputSymlink> getOutputSymlinks() {
648+
return actionResult.getOutputSymlinksList();
649+
}
650+
647651
/**
648652
* Returns the freeform informational message with details on the execution of the action that
649653
* may be displayed to the user upon failure or when requested explicitly.
@@ -1064,7 +1068,7 @@ ActionResultMetadata parseActionResultMetadata(
10641068
directExecutor()));
10651069
}
10661070

1067-
waitForBulkTransfer(dirMetadataDownloads.values(), /* cancelRemainingOnInterrupt=*/ true);
1071+
waitForBulkTransfer(dirMetadataDownloads.values(), /* cancelRemainingOnInterrupt= */ true);
10681072

10691073
ImmutableMap.Builder<Path, DirectoryMetadata> directories = ImmutableMap.builder();
10701074
for (Map.Entry<Path, ListenableFuture<Tree>> metadataDownload :
@@ -1088,18 +1092,33 @@ ActionResultMetadata parseActionResultMetadata(
10881092
new FileMetadata(localPath, outputFile.getDigest(), outputFile.getIsExecutable()));
10891093
}
10901094

1091-
ImmutableMap.Builder<Path, SymlinkMetadata> symlinks = ImmutableMap.builder();
1092-
Iterable<OutputSymlink> outputSymlinks =
1093-
Iterables.concat(result.getOutputFileSymlinks(), result.getOutputDirectorySymlinks());
1094-
for (OutputSymlink symlink : outputSymlinks) {
1095-
Path localPath =
1095+
var symlinkMap = new HashMap<Path, SymlinkMetadata>();
1096+
var outputSymlinks =
1097+
Iterables.concat(
1098+
result.getOutputFileSymlinks(),
1099+
result.getOutputDirectorySymlinks(),
1100+
result.getOutputSymlinks());
1101+
for (var symlink : outputSymlinks) {
1102+
var localPath =
10961103
remotePathResolver.outputPathToLocalPath(encodeBytestringUtf8(symlink.getPath()));
1097-
symlinks.put(
1098-
localPath, new SymlinkMetadata(localPath, PathFragment.create(symlink.getTarget())));
1104+
var target = PathFragment.create(symlink.getTarget());
1105+
var existingMetadata = symlinkMap.get(localPath);
1106+
if (existingMetadata != null) {
1107+
if (!target.equals(existingMetadata.target())) {
1108+
throw new IOException(
1109+
String.format(
1110+
"Symlink path collision: '%s' is mapped to both '%s' and '%s'. Action Result"
1111+
+ " should not contain multiple targets for the same symlink.",
1112+
localPath, existingMetadata.target(), target));
1113+
}
1114+
continue;
1115+
}
1116+
1117+
symlinkMap.put(localPath, new SymlinkMetadata(localPath, target));
10991118
}
11001119

11011120
return new ActionResultMetadata(
1102-
files.buildOrThrow(), symlinks.buildOrThrow(), directories.buildOrThrow());
1121+
files.buildOrThrow(), ImmutableMap.copyOf(symlinkMap), directories.buildOrThrow());
11031122
}
11041123

11051124
/**
@@ -1244,7 +1263,7 @@ public InMemoryOutput downloadOutputs(RemoteAction action, RemoteActionResult re
12441263
ListenableFuture<byte[]> inMemoryOutputDownload =
12451264
remoteCache.downloadBlob(context, inMemoryOutputDigest);
12461265
waitForBulkTransfer(
1247-
ImmutableList.of(inMemoryOutputDownload), /* cancelRemainingOnInterrupt=*/ true);
1266+
ImmutableList.of(inMemoryOutputDownload), /* cancelRemainingOnInterrupt= */ true);
12481267
byte[] data = getFromFuture(inMemoryOutputDownload);
12491268
return new InMemoryOutput(inMemoryOutput, ByteString.copyFrom(data));
12501269
}

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
@@ -590,6 +590,54 @@ public void downloadOutputs_relativeDirectorySymlink_success() throws Exception
590590
assertThat(context.isLockOutputFilesCalled()).isTrue();
591591
}
592592

593+
@Test
594+
public void downloadOutputs_relativeOutputSymlinks_success() throws Exception {
595+
// Test that download outputs works when the action result only contains output_symlinks
596+
// and not output_file_symlinks or output_directory_symlinks, which are deprecated.
597+
ActionResult.Builder builder = ActionResult.newBuilder();
598+
builder.addOutputSymlinksBuilder().setPath("outputs/a/b/link").setTarget("../../foo");
599+
RemoteActionResult result =
600+
RemoteActionResult.createFromCache(CachedActionResult.remote(builder.build()));
601+
Spawn spawn = newSpawnFromResult(result);
602+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
603+
RemoteExecutionService service = newRemoteExecutionService();
604+
RemoteAction action = service.buildRemoteAction(spawn, context);
605+
606+
// Doesn't check for dangling links, hence download succeeds.
607+
service.downloadOutputs(action, result);
608+
609+
Path path = execRoot.getRelative("outputs/a/b/link");
610+
assertThat(path.isSymbolicLink()).isTrue();
611+
assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("../../foo"));
612+
assertThat(context.isLockOutputFilesCalled()).isTrue();
613+
}
614+
615+
@Test
616+
public void downloadOutputs_outputSymlinksCompatibility_success() throws Exception {
617+
// Test that download outputs works when the action result contains both output_symlinks
618+
// and output_file_symlinks (or output_directory_symlinks).
619+
//
620+
// Remote Execution Server may set both fields to ensure backward compatibility with
621+
// clients that don't support output_symlinks.
622+
ActionResult.Builder builder = ActionResult.newBuilder();
623+
builder.addOutputFileSymlinksBuilder().setPath("outputs/a/b/link").setTarget("foo");
624+
builder.addOutputSymlinksBuilder().setPath("outputs/a/b/link").setTarget("foo");
625+
RemoteActionResult result =
626+
RemoteActionResult.createFromCache(CachedActionResult.remote(builder.build()));
627+
Spawn spawn = newSpawnFromResult(result);
628+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
629+
RemoteExecutionService service = newRemoteExecutionService();
630+
RemoteAction action = service.buildRemoteAction(spawn, context);
631+
632+
// Doesn't check for dangling links, hence download succeeds.
633+
service.downloadOutputs(action, result);
634+
635+
Path path = execRoot.getRelative("outputs/a/b/link");
636+
assertThat(path.isSymbolicLink()).isTrue();
637+
assertThat(path.readSymbolicLink()).isEqualTo(PathFragment.create("foo"));
638+
assertThat(context.isLockOutputFilesCalled()).isTrue();
639+
}
640+
593641
@Test
594642
public void downloadOutputs_relativeSymlinkInDirectory_success() throws Exception {
595643
Tree tree =
@@ -681,6 +729,28 @@ public void downloadOutputs_absoluteSymlinkInDirectory_error() throws Exception
681729
assertThat(context.isLockOutputFilesCalled()).isTrue();
682730
}
683731

732+
@Test
733+
public void downloadOutputs_symlinkCollision_error() throws Exception {
734+
ActionResult.Builder builder = ActionResult.newBuilder();
735+
builder.addOutputDirectorySymlinksBuilder().setPath("outputs/foo").setTarget("foo1");
736+
builder.addOutputSymlinksBuilder().setPath("outputs/foo").setTarget("foo2");
737+
RemoteActionResult result =
738+
RemoteActionResult.createFromCache(CachedActionResult.remote(builder.build()));
739+
Spawn spawn = newSpawnFromResult(result);
740+
FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn);
741+
RemoteExecutionService service = newRemoteExecutionService();
742+
RemoteAction action = service.buildRemoteAction(spawn, context);
743+
744+
IOException expected =
745+
assertThrows(IOException.class, () -> service.downloadOutputs(action, result));
746+
747+
assertThat(expected.getSuppressed()).isEmpty();
748+
assertThat(expected).hasMessageThat().contains("Symlink path collision");
749+
assertThat(expected).hasMessageThat().contains("outputs/foo");
750+
assertThat(expected).hasMessageThat().contains("foo1");
751+
assertThat(expected).hasMessageThat().contains("foo2");
752+
}
753+
684754
@Test
685755
public void downloadOutputs_onFailure_maintainDirectories() throws Exception {
686756
// Test that output directories are not deleted on download failure. See
@@ -2014,6 +2084,12 @@ private Spawn newSpawnFromResult(
20142084
outputs.add(output);
20152085
}
20162086

2087+
for (OutputSymlink symlink : result.getOutputSymlinks()) {
2088+
Path path = remotePathResolver.outputPathToLocalPath(symlink.getPath());
2089+
Artifact output = ActionsTestUtil.createArtifact(artifactRoot, path);
2090+
outputs.add(output);
2091+
}
2092+
20172093
return newSpawn(executionInfo, outputs.build());
20182094
}
20192095

0 commit comments

Comments
 (0)