Propagate IOExceptions through RecursiveFilesystemTraversalFunction.

Both PackageLookupFunction and DirectoryListingFunction can throw IOExceptions (wrapped in BuildFileNotFoundException, in the case of the former) that should be processed and not silently propagated up.

PiperOrigin-RevId: 361228178
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 f742794..8cfda43 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
@@ -32,6 +32,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
 import com.google.devtools.build.lib.profiler.SilentCloseable;
@@ -64,9 +65,6 @@
 
 /** A {@link SkyFunction} to build {@link RecursiveFilesystemTraversalValue}s. */
 public final class RecursiveFilesystemTraversalFunction implements SkyFunction {
-
-  private static final class MissingDepException extends Exception {}
-
   private static final byte[] MISSING_FINGERPRINT =
       new BigInteger(1, "NonexistentFileStateValue".getBytes(UTF_8)).toByteArray();
 
@@ -98,6 +96,9 @@
 
       /** A generated directory's root-relative path conflicts with a package's path. */
       GENERATED_PATH_CONFLICT,
+
+      /** A file/directory visited was part of a symlink cycle or infinite expansion. */
+      SYMLINK_CYCLE_OR_INFINITE_EXPANSION,
     }
 
     private final Type type;
@@ -138,6 +139,7 @@
     }
   }
 
+  @Nullable
   @Override
   public SkyValue compute(SkyKey skyKey, Environment env)
       throws RecursiveFilesystemTraversalFunctionException, InterruptedException {
@@ -147,6 +149,9 @@
             .profile(ProfilerTask.FILESYSTEM_TRAVERSAL, traversal.getRoot().toString())) {
       // Stat the traversal root.
       FileInfo rootInfo = lookUpFileInfo(env, traversal);
+      if (rootInfo == null) {
+        return null;
+      }
 
       if (!rootInfo.type.exists()) {
         // May be a dangling symlink or a non-existent file. Handle gracefully.
@@ -179,6 +184,9 @@
 
       // Otherwise the root is a directory or a symlink to one.
       PkgLookupResult pkgLookupResult = checkIfPackage(env, traversal, rootInfo);
+      if (pkgLookupResult == null) {
+        return null;
+      }
       traversal = pkgLookupResult.traversal;
 
       if (pkgLookupResult.isConflicting()) {
@@ -211,17 +219,28 @@
 
       // We are free to traverse this directory.
       Collection<SkyKey> dependentKeys = createRecursiveTraversalKeys(env, traversal);
-      return resultForDirectory(
-          traversal,
-          rootInfo,
-          traverseChildren(env, dependentKeys, /*inline=*/ traversal.isRootGenerated));
-    } catch (IOException e) {
-      String message = "Error while traversing fileset: " + e.getMessage();
+      if (dependentKeys == null) {
+        return null;
+      }
+      Collection<RecursiveFilesystemTraversalValue> subdirTraversals =
+          traverseChildren(env, dependentKeys, /*inline=*/ traversal.isRootGenerated);
+      if (subdirTraversals == null) {
+        return null;
+      }
+      return resultForDirectory(traversal, rootInfo, subdirTraversals);
+    } catch (IOException | BuildFileNotFoundException e) {
+      String message =
+          String.format(
+              "Error while traversing directory %s: %s",
+              traversal.root.getRelativePart(), e.getMessage());
       throw new RecursiveFilesystemTraversalFunctionException(
           new RecursiveFilesystemTraversalException(
-              message, RecursiveFilesystemTraversalException.Type.FILE_OPERATION_FAILURE));
-    } catch (MissingDepException e) {
-      return null;
+              message,
+              // Trying to stat the starting point of this root may have failed with a symlink cycle
+              // or trying to get a package lookup value may have failed due to a symlink cycle.
+              e instanceof FileSymlinkException || e.getCause() instanceof FileSymlinkException
+                  ? RecursiveFilesystemTraversalException.Type.SYMLINK_CYCLE_OR_INFINITE_EXPANSION
+                  : RecursiveFilesystemTraversalException.Type.FILE_OPERATION_FAILURE));
     }
   }
 
@@ -272,8 +291,9 @@
     }
   }
 
+  @Nullable
   private static FileInfo lookUpFileInfo(Environment env, TraversalRequest traversal)
-      throws MissingDepException, IOException, InterruptedException {
+      throws IOException, InterruptedException {
     if (traversal.isRootGenerated) {
       HasDigest fsVal = null;
       if (traversal.root.getOutputArtifact() != null) {
@@ -281,7 +301,7 @@
         SkyKey artifactKey = Artifact.key(artifact);
         SkyValue value = env.getValue(artifactKey);
         if (env.valuesMissing()) {
-          throw new MissingDepException();
+          return null;
         }
 
         if (value instanceof FileArtifactValue || value instanceof TreeArtifactValue) {
@@ -341,7 +361,7 @@
               FileValue.key(traversal.root.asRootedPath()), IOException.class);
 
       if (env.valuesMissing()) {
-        throw new MissingDepException();
+        return null;
       }
       if (fileValue.unboundedAncestorSymlinkExpansionChain() != null) {
         SkyKey uniquenessKey =
@@ -349,7 +369,7 @@
                 fileValue.unboundedAncestorSymlinkExpansionChain());
         env.getValue(uniquenessKey);
         if (env.valuesMissing()) {
-          throw new MissingDepException();
+          return null;
         }
 
         throw new FileSymlinkInfiniteExpansionException(
@@ -477,13 +497,22 @@
    */
   private static PkgLookupResult checkIfPackage(
       Environment env, TraversalRequest traversal, FileInfo rootInfo)
-      throws MissingDepException, IOException, InterruptedException {
+      throws IOException, InterruptedException, BuildFileNotFoundException {
     Preconditions.checkArgument(rootInfo.type.exists() && !rootInfo.type.isFile(),
         "{%s} {%s}", traversal, rootInfo);
+    // PackageLookupFunction/dependencies can only throw IOException, BuildFileNotFoundException,
+    // and RepositoryFetchException, and RepositoryFetchException is not in play here. Note that
+    // run-of-the-mill circular symlinks will *not* throw here, and will trigger later errors during
+    // the recursive traversal.
     PackageLookupValue pkgLookup =
         (PackageLookupValue)
-            getDependentSkyValue(env,
-                PackageLookupValue.key(traversal.root.asRootedPath().getRootRelativePath()));
+            env.getValueOrThrow(
+                PackageLookupValue.key(traversal.root.asRootedPath().getRootRelativePath()),
+                BuildFileNotFoundException.class,
+                IOException.class);
+    if (env.valuesMissing()) {
+      return null;
+    }
 
     if (pkgLookup.packageExists()) {
       if (traversal.isRootGenerated) {
@@ -514,9 +543,9 @@
    *
    * <p>The returned keys are of type {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL}.
    */
+  @Nullable
   private static Collection<SkyKey> createRecursiveTraversalKeys(
-      Environment env, TraversalRequest traversal)
-      throws MissingDepException, InterruptedException, IOException {
+      Environment env, TraversalRequest traversal) throws InterruptedException, IOException {
     // Use the traversal's path, even if it's a symlink. The contents of the directory, as listed
     // in the result, must be relative to it.
     Iterable<Dirent> dirents;
@@ -529,8 +558,14 @@
       Collections.sort(direntsCollection);
       dirents = direntsCollection;
     } else {
-      dirents = ((DirectoryListingValue) getDependentSkyValue(env,
-          DirectoryListingValue.key(traversal.root.asRootedPath()))).getDirents();
+      DirectoryListingValue dirListingValue =
+          (DirectoryListingValue)
+              env.getValueOrThrow(
+                  DirectoryListingValue.key(traversal.root.asRootedPath()), IOException.class);
+      if (dirListingValue == null) {
+        return null;
+      }
+      dirents = dirListingValue.getDirents();
     }
 
     List<SkyKey> result = new ArrayList<>();
@@ -618,24 +653,15 @@
     return new HasDigest.ByteStringDigest(result);
   }
 
-  private static SkyValue getDependentSkyValue(Environment env, SkyKey key)
-      throws MissingDepException, InterruptedException {
-    SkyValue value = env.getValue(key);
-    if (env.valuesMissing()) {
-      throw new MissingDepException();
-    }
-    return value;
-  }
-
   /**
    * Requests Skyframe to compute the dependent values and returns them.
    *
    * <p>The keys must all be {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL} keys.
    */
+  @Nullable
   private Collection<RecursiveFilesystemTraversalValue> traverseChildren(
       Environment env, Iterable<SkyKey> keys, boolean inline)
-      throws MissingDepException, InterruptedException,
-      RecursiveFilesystemTraversalFunctionException {
+      throws InterruptedException, RecursiveFilesystemTraversalFunctionException {
     Map<SkyKey, SkyValue> values;
     if (inline) {
       // Don't create Skyframe nodes for a recursive traversal over the output tree.
@@ -648,7 +674,7 @@
       values = env.getValues(keys);
     }
     if (env.valuesMissing()) {
-      throw new MissingDepException();
+      return null;
     }
 
     return Collections2.transform(values.values(), RecursiveFilesystemTraversalValue.class::cast);