Gracefully handle filesystem inconsistencies in `RecursiveFilesystemTraversalFunction`.

PiperOrigin-RevId: 577878638
Change-Id: I696e1ace5338a026345ece2222b620cea99ae264
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 8c2a068..62e2ca4 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
@@ -319,6 +319,9 @@
         case SYMLINK_CYCLE_OR_INFINITE_EXPANSION:
           throw new ArtifactFunctionException(
               SourceArtifactException.create(artifact, e), Transience.PERSISTENT);
+        case INCONSISTENT_FILESYSTEM:
+          throw new ArtifactFunctionException(
+              SourceArtifactException.create(artifact, e), Transience.TRANSIENT);
         case CANNOT_CROSS_PACKAGE_BOUNDARY:
           throw new IllegalStateException(
               String.format(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index cc6855b..a39bc53 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -2003,6 +2003,7 @@
         "//src/main/java/com/google/devtools/build/lib/io:file_symlink_exception",
         "//src/main/java/com/google/devtools/build/lib/io:file_symlink_infinite_expansion_exception",
         "//src/main/java/com/google/devtools/build/lib/io:file_symlink_infinite_expansion_uniqueness_function",
+        "//src/main/java/com/google/devtools/build/lib/io:inconsistent_filesystem_exception",
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
index 70fa69d..6c03cb3 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
@@ -15,7 +15,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -36,6 +35,7 @@
 import com.google.devtools.build.lib.io.FileSymlinkException;
 import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionException;
 import com.google.devtools.build.lib.io.FileSymlinkInfiniteExpansionUniquenessFunction;
+import com.google.devtools.build.lib.io.InconsistentFilesystemException;
 import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
@@ -106,6 +106,9 @@
 
       /** A file/directory visited was part of a symlink cycle or infinite expansion. */
       SYMLINK_CYCLE_OR_INFINITE_EXPANSION,
+
+      /** The filesystem told us inconsistent information. */
+      INCONSISTENT_FILESYSTEM,
     }
 
     private final RecursiveFilesystemTraversalException.Type type;
@@ -170,7 +173,11 @@
       if (!rootInfo.type.exists()) {
         // May be a dangling symlink or a non-existent file. Handle gracefully.
         if (rootInfo.type.isSymlink()) {
-          return resultForDanglingSymlink(traversal.root().asRootedPath(), rootInfo);
+          return RecursiveFilesystemTraversalValue.of(
+              ResolvedFileFactory.danglingSymlink(
+                  traversal.root().asRootedPath(),
+                  rootInfo.unresolvedSymlinkTarget,
+                  rootInfo.metadata));
         } else {
           return RecursiveFilesystemTraversalValue.EMPTY;
         }
@@ -248,6 +255,9 @@
       // trying to get a package lookup value may have failed due to a symlink cycle.
       RecursiveFilesystemTraversalException.Type exceptionType =
           RecursiveFilesystemTraversalException.Type.FILE_OPERATION_FAILURE;
+      if (e instanceof InconsistentFilesystemException) {
+        exceptionType = RecursiveFilesystemTraversalException.Type.INCONSISTENT_FILESYSTEM;
+      }
       if (e instanceof FileSymlinkException) {
         exceptionType =
             RecursiveFilesystemTraversalException.Type.SYMLINK_CYCLE_OR_INFINITE_EXPANSION;
@@ -570,27 +580,19 @@
   }
 
   /**
-   * Creates result for a dangling symlink.
-   *
-   * @param linkName path to the symbolic link
-   * @param info the {@link FileInfo} associated with the link file
-   */
-  private static RecursiveFilesystemTraversalValue resultForDanglingSymlink(
-      RootedPath linkName, FileInfo info) {
-    checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName, info.type);
-    return RecursiveFilesystemTraversalValue.of(
-        ResolvedFileFactory.danglingSymlink(linkName, info.unresolvedSymlinkTarget, info.metadata));
-  }
-
-  /**
    * Creates results for a file or for a symlink that points to one.
    *
    * <p>A symlink may be direct (points to a file) or transitive (points at a direct or transitive
    * symlink).
    */
-  private static RecursiveFilesystemTraversalValue resultForFileRoot(
-      RootedPath path, FileInfo info) {
-    checkState(info.type.isFile() && info.type.exists(), "{%s} {%s}", path, info.type);
+  private static RecursiveFilesystemTraversalValue resultForFileRoot(RootedPath path, FileInfo info)
+      throws InconsistentFilesystemException {
+    if (!info.type.isFile() || !info.type.exists()) {
+      throw new InconsistentFilesystemException(
+          String.format(
+              "We were previously told %s was an existing file but it's actually %s", path, info));
+    }
+
     if (info.type.isSymlink()) {
       return RecursiveFilesystemTraversalValue.of(
           ResolvedFileFactory.symlinkToFile(
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
index abad770d..e4c01dc 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
@@ -70,8 +70,10 @@
 import com.google.devtools.build.lib.testutil.TimestampGranularityUtils;
 import com.google.devtools.build.lib.util.io.OutErr;
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.DelegateFileSystem;
 import com.google.devtools.build.lib.vfs.FileStateKey;
 import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
@@ -125,6 +127,22 @@
   private NonHermeticArtifactFakeFunction artifactFunction;
   private List<Artifact.DerivedArtifact> artifacts;
 
+  private final Set<PathFragment> pathsToPretendDontExist = Sets.newConcurrentHashSet();
+
+  @Override
+  protected FileSystem createFileSystem() {
+    return new DelegateFileSystem(super.createFileSystem()) {
+      @Override
+      protected FileStatus statIfFound(PathFragment path, boolean followSymlinks)
+          throws IOException {
+        if (pathsToPretendDontExist.contains(path)) {
+          return null;
+        }
+        return super.statIfFound(path, followSymlinks);
+      }
+    };
+  }
+
   @Before
   public void setUp() {
     artifacts = new ArrayList<>();
@@ -1254,6 +1272,24 @@
     assertThat(result.getDigest()).isEqualTo(expectedBytes);
   }
 
+  @Test
+  public void testGracefullyHandlesInconsistentFilesystem() throws Exception {
+    scratch.dir("parent");
+    PathFragment childPathFragment = scratch.file("parent/child").asFragment();
+    pathsToPretendDontExist.add(childPathFragment);
+    Artifact childArtifact = sourceArtifact("parent/child");
+    SkyKey key = pkgRoot(parentOf(rootedPath(childArtifact)), DONT_CROSS);
+    EvaluationResult<SkyValue> result = eval(key);
+    assertThat(result.hasError()).isTrue();
+    ErrorInfo error = result.getError(key);
+    assertThat(error.getException()).isInstanceOf(RecursiveFilesystemTraversalException.class);
+    assertThat(((RecursiveFilesystemTraversalException) error.getException()).getType())
+        .isEqualTo(RecursiveFilesystemTraversalException.Type.INCONSISTENT_FILESYSTEM);
+    assertThat(error.getException())
+        .hasMessageThat()
+        .contains("We were previously told [/workspace]/[parent/child] was an existing file but");
+  }
+
   private static class NonHermeticArtifactSkyKey extends AbstractSkyKey<SkyKey> {
     private NonHermeticArtifactSkyKey(SkyKey arg) {
       super(arg);