diff --git a/src/main/java/com/google/devtools/build/lib/remote/AbstractActionInputPrefetcher.java b/src/main/java/com/google/devtools/build/lib/remote/AbstractActionInputPrefetcher.java index 3a2769cf467ee9..8751d5b2997184 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/AbstractActionInputPrefetcher.java +++ b/src/main/java/com/google/devtools/build/lib/remote/AbstractActionInputPrefetcher.java @@ -47,6 +47,7 @@ import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.profiler.Profiler; import com.google.devtools.build.lib.profiler.ProfilerTask; +import com.google.devtools.build.lib.remote.common.CacheNotFoundException; import com.google.devtools.build.lib.remote.util.AsyncTaskCache; import com.google.devtools.build.lib.util.TempPathGenerator; import com.google.devtools.build.lib.vfs.FileSymlinkLoopException; @@ -394,14 +395,24 @@ private ListenableFuture prefetchFile( Completable result = downloadFileNoCheckRx( - action, - execRoot.getRelative(execPath), - treeRootExecPath != null ? execRoot.getRelative(treeRootExecPath) : null, - dirsWithOutputPermissions, - input, - metadata, - priority, - reason); + action, + execRoot.getRelative(execPath), + treeRootExecPath != null ? execRoot.getRelative(treeRootExecPath) : null, + dirsWithOutputPermissions, + input, + metadata, + priority, + reason) + .onErrorResumeNext( + t -> { + if (t instanceof CacheNotFoundException cacheNotFoundException) { + // Only the symlink itself is guaranteed to be an input to the action, so + // report its path for rewinding. + cacheNotFoundException.setExecPath(input.getExecPath()); + return Completable.error(cacheNotFoundException); + } + return Completable.error(t); + }); if (symlink != null) { result = result.andThen(plantSymlink(symlink)); diff --git a/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java index 423546c301f85a..55f08a12644024 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTest.java @@ -418,6 +418,127 @@ public void remoteCacheEvictBlobs_whenPrefetchingInput_succeedsWithActionRewindi assertValidOutputFile("a/bar.out", "foo" + lineSeparator() + "updated bar" + lineSeparator()); } + @Test + public void remoteCacheEvictBlobs_whenPrefetchingSymlinkedInput_exitWithCode39() + throws Exception { + // Arrange: Prepare workspace and populate remote cache + writeSymlinkRule(); + write( + "a/BUILD", + """ + load("//:symlink.bzl", "symlink") + + genrule( + name = "foo", + srcs = ["foo.in"], + outs = ["foo.out"], + cmd = "cat $(SRCS) > $@", + ) + + symlink( + name = "symlinked_foo", + target_artifact = ":foo.out", + ) + + genrule( + name = "bar", + srcs = [ + ":symlinked_foo", + "bar.in", + ], + outs = ["bar.out"], + cmd = "cat $(SRCS) > $@", + tags = ["no-remote-exec"], + ) + """); + write("a/foo.in", "foo"); + write("a/bar.in", "bar"); + + // Populate remote cache + buildTarget("//a:bar"); + var bytes = readContent(getOutputPath("a/foo.out")); + var hashCode = getDigestHashFunction().getHashFunction().hashBytes(bytes); + getOnlyElement(getArtifacts("//a:symlinked_foo")).getPath().delete(); + getOutputPath("a/foo.out").delete(); + getOutputPath("a/bar.out").delete(); + getOutputBase().getRelative("action_cache").deleteTreesBelow(); + restartServer(); + + // Clean build, foo.out isn't downloaded + buildTarget("//a:bar"); + assertOutputDoesNotExist("a/foo.out"); + assertOutputsDoNotExist("//a:symlinked_foo"); + + // Act: Evict blobs from remote cache and do an incremental build + evictAllBlobs(); + write("a/bar.in", "updated bar"); + var error = assertThrows(BuildFailedException.class, () -> buildTarget("//a:bar")); + + // Assert: Exit code is 39 + assertThat(error).hasMessageThat().contains("Lost inputs no longer available remotely"); + assertThat(error).hasMessageThat().contains("a/symlinked_foo"); + assertThat(error).hasMessageThat().contains(String.format("%s/%s", hashCode, bytes.length)); + assertThat(error.getDetailedExitCode().getExitCode().getNumericExitCode()).isEqualTo(39); + } + + @Test + public void remoteCacheEvictBlobs_whenPrefetchingSymlinkedInput_succeedsWithActionRewinding() + throws Exception { + writeSymlinkRule(); + write( + "a/BUILD", + """ + load("//:symlink.bzl", "symlink") + + genrule( + name = "foo", + srcs = ["foo.in"], + outs = ["foo.out"], + cmd = "cat $(SRCS) > $@", + ) + + symlink( + name = "symlinked_foo", + target_artifact = ":foo.out", + ) + + genrule( + name = "bar", + srcs = [ + ":symlinked_foo", + "bar.in", + ], + outs = ["bar.out"], + cmd = "cat $(SRCS) > $@", + tags = ["no-remote-exec"], + ) + """); + write("a/foo.in", "foo"); + write("a/bar.in", "bar"); + + // Populate remote cache + buildTarget("//a:bar"); + getOnlyElement(getArtifacts("//a:symlinked_foo")).getPath().delete(); + getOutputPath("a/foo.out").delete(); + getOutputPath("a/bar.out").delete(); + getOutputBase().getRelative("action_cache").deleteTreesBelow(); + restartServer(); + + // Clean build, foo.out isn't downloaded + buildTarget("//a:bar"); + assertOutputDoesNotExist("a/foo.out"); + assertOutputsDoNotExist("//a:symlinked_foo"); + + // Act: Evict blobs from remote cache and do an incremental build + evictAllBlobs(); + write("a/bar.in", "updated bar"); + enableActionRewinding(); + buildTarget("//a:bar"); + + // Assert: target was successfully built + assertValidOutputFile("a/bar.out", "foo" + lineSeparator() + "updated bar" + lineSeparator()); + } + @Test public void remoteCacheEvictBlobs_whenUploadingInput_exitWithCode39() throws Exception { // Arrange: Prepare workspace and populate remote cache diff --git a/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTestBase.java b/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTestBase.java index b53b8f82770efd..2a9041c94023be 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTestBase.java +++ b/src/test/java/com/google/devtools/build/lib/remote/BuildWithoutTheBytesIntegrationTestBase.java @@ -37,6 +37,7 @@ import com.google.devtools.build.lib.util.CommandBuilder; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.io.RecordingOutErr; +import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import java.io.IOException; @@ -2033,6 +2034,7 @@ protected void assertSymlink(String binRelativeLinkPath, PathFragment targetPath } protected void writeSymlinkRule() throws IOException { + FileSystemUtils.touchFile(getWorkspace().getRelative("BUILD")); write( "symlink.bzl", """