Add FileDependencyDeserializer.getDirectoryListingDependencies.

The implementation is closely resembles that of getFileDependencies.

* Adds the DirectoryListingDependencies type to contain the result.

* Creates common base class FileSystemDependencies, for
  DirectoryListingDependencies and FileDependencies. These classes will
  be put into the same containers.

* Pulls FileDependencyDeserializer.FutureFileDependencies out into
  SettableFutureWithOwnership and makes it generic for reuse.

PiperOrigin-RevId: 684818562
Change-Id: I963276a534b04e19b17f390c85d0d5a73cf0f481
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD
index de25f48..dc5af33 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD
@@ -81,11 +81,14 @@
 java_library(
     name = "file_dependency_deserializer",
     srcs = [
+        "DirectoryListingDependencies.java",
         "FileDependencies.java",
         "FileDependencyDeserializer.java",
+        "FileSystemDependencies.java",
     ],
     deps = [
         ":file_dependency_key_support",
+        ":settable_future_with_ownership",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
         "//src/main/java/com/google/devtools/build/lib/vfs:ospathpolicy",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
@@ -99,6 +102,12 @@
 )
 
 java_library(
+    name = "settable_future_with_ownership",
+    srcs = ["SettableFutureWithOwnership.java"],
+    deps = ["//third_party:guava"],
+)
+
+java_library(
     name = "file_dependency_key_support",
     srcs = ["FileDependencyKeySupport.java"],
     deps = [
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/DirectoryListingDependencies.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/DirectoryListingDependencies.java
new file mode 100644
index 0000000..a6df57f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/DirectoryListingDependencies.java
@@ -0,0 +1,26 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe.serialization.analysis;
+
+import com.google.common.collect.ImmutableSet;
+
+/** Type representing a directory listing operation. */
+record DirectoryListingDependencies(FileDependencies realDirectory)
+    implements FileDependencyDeserializer.GetDirectoryListingDependenciesResult,
+        FileSystemDependencies {
+  /** True if this entry matches any directory name in {@code directoryPaths}. */
+  boolean matchesAnyDirectory(ImmutableSet<String> directoryPaths) {
+    return directoryPaths.contains(realDirectory.resolvedPath());
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencies.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencies.java
index ccd9c3e..01ad942 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencies.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencies.java
@@ -28,7 +28,8 @@
  * #getDependencyCount} and {@link #getDependency}. If any matches are encountered, the associated
  * value is invalidated.
  */
-sealed interface FileDependencies extends FileDependencyDeserializer.GetDependenciesResult
+sealed interface FileDependencies
+    extends FileSystemDependencies, FileDependencyDeserializer.GetDependenciesResult
     permits FileDependencies.SingleResolvedPath,
         FileDependencies.SingleResolvedPathAndDependency,
         FileDependencies.MultiplePaths {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencyDeserializer.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencyDeserializer.java
index d4c954a..da50172 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencyDeserializer.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencyDeserializer.java
@@ -17,6 +17,7 @@
 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
 import static com.google.common.util.concurrent.Futures.immediateFuture;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static com.google.devtools.build.lib.skyframe.serialization.analysis.FileDependencyKeySupport.DIRECTORY_KEY_DELIMITER;
 import static com.google.devtools.build.lib.skyframe.serialization.analysis.FileDependencyKeySupport.FILE_KEY_DELIMITER;
 import static com.google.devtools.build.lib.skyframe.serialization.analysis.FileDependencyKeySupport.MAX_KEY_LENGTH;
 import static com.google.devtools.build.lib.skyframe.serialization.analysis.FileDependencyKeySupport.MTSV_SENTINEL;
@@ -27,7 +28,7 @@
 
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
-import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.base.Function;
 import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -35,34 +36,47 @@
 import com.google.devtools.build.lib.skyframe.serialization.KeyBytesProvider;
 import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
 import com.google.devtools.build.lib.skyframe.serialization.StringKey;
+import com.google.devtools.build.lib.skyframe.serialization.proto.DirectoryListingInvalidationData;
 import com.google.devtools.build.lib.skyframe.serialization.proto.FileInvalidationData;
 import com.google.devtools.build.lib.skyframe.serialization.proto.Symlink;
 import com.google.devtools.build.lib.vfs.OsPathPolicy;
 import com.google.protobuf.InvalidProtocolBufferException;
 import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.VarHandle;
 import javax.annotation.Nullable;
 
 /**
  * Deserializes dependency information persisted by {@link FileDependencySerializer}.
  *
- * <p>Fetching a dependency is a mostly linear asynchronous state machine that performs actions then
- * waits in an alternating manner.
+ * <p>Fetching a file dependency is a mostly linear asynchronous state machine that performs actions
+ * then waits in an alternating manner.
  *
  * <ol>
  *   <li>Request the data for a given key.
- *   <li>{@link WaitForData}.
+ *   <li>{@link WaitForFileInvalidationData}.
  *   <li>Request the data for the parent directory (a recursive call).
  *   <li>{@link WaitForParent}.
  *   <li>Process any symlinks, resolving symlink parents as needed.
  *   <li>{@link WaitForSymlinkParent}.
  *   <li>Processing symlinks repeats for all the symlinks associated with an entry.
  * </ol>
+ *
+ * <p>A similar, but simpler state machine is used for directory listings.
+ *
+ * <ol>
+ *   <li>Request the data for a given key.
+ *   <li>{@link WaitForListingInvalidationData}.
+ *   <li>Request the file data corresponding to the directory (delegating to {@link
+ *       #getFileDependencies}).
+ *   <li>{@link WaitForListingFileDependencies}.
+ *   <li>Create and cache the {@link DirectoryListingDependencies} instance.
+ * </ol>
  */
 final class FileDependencyDeserializer {
   private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs();
 
+  /** Singleton representing the root file. */
+  static final FileDependencies ROOT_FILE = FileDependencies.builder("").build();
+
   private final FingerprintValueService fingerprintValueService;
 
   /**
@@ -79,8 +93,16 @@
    * retained by the {@code SkyValue}s that depend on them. When all such associated {@code
    * SkyValue}s are invalidated, the dependency information becomes eligible for GC.
    */
-  private final Cache<String, GetDependenciesResult> dependenciesCache =
-      Caffeine.newBuilder().weakValues().<String, GetDependenciesResult>build();
+  private final Cache<String, GetDependenciesResult> fileCache =
+      Caffeine.newBuilder().weakValues().build();
+
+  /**
+   * A cache for {@link DirectoryListingDependencies}, primarily for deduplication.
+   *
+   * <p>This follows the design of {@link #fileCache} but is for directory listings.
+   */
+  private final Cache<String, GetDirectoryListingDependenciesResult> listingCache =
+      Caffeine.newBuilder().weakValues().build();
 
   FileDependencyDeserializer(FingerprintValueService fingerprintValueService) {
     this.fingerprintValueService = fingerprintValueService;
@@ -89,6 +111,15 @@
   sealed interface GetDependenciesResult permits FileDependencies, FutureFileDependencies {}
 
   /**
+   * The main purpose of this class is to act as a {@link ListenableFuture<FileDependencies>}.
+   *
+   * <p>Its specific type is explicitly visible to clients to allow them to cleanly distinguish it
+   * as a permitted subtype of {@link GetDependenciesResult}.
+   */
+  static final class FutureFileDependencies extends SettableFutureWithOwnership<FileDependencies>
+      implements GetDependenciesResult {}
+
+  /**
    * Reconstitutes the set of file dependencies associated with {@code key}.
    *
    * <p>Performs lookups and parent resolution (recursively) and symlink resolution to obtain all
@@ -100,7 +131,7 @@
    */
   GetDependenciesResult getFileDependencies(String key) {
     FutureFileDependencies ownedFuture;
-    switch (dependenciesCache.get(key, unused -> new FutureFileDependencies())) {
+    switch (fileCache.get(key, unused -> new FutureFileDependencies())) {
       case FileDependencies dependencies:
         return dependencies;
       case FutureFileDependencies future:
@@ -111,79 +142,54 @@
         break;
     }
     // `ownedFuture` is owned by this thread, which must complete its value.
-    try {
-      ListenableFuture<byte[]> futureBytes;
-      try {
-        futureBytes = fingerprintValueService.get(getKeyBytes(key));
-      } catch (IOException e) {
-        ownedFuture.setIoException(e);
-        return ownedFuture;
-      }
-
-      ownedFuture.setFutureFiles(
-          Futures.transformAsync(
-              futureBytes, new WaitForData(key), fingerprintValueService.getExecutor()));
-      return ownedFuture;
-    } finally {
-      ownedFuture.verifySet();
-    }
+    fetchInvalidationData(key, WaitForFileInvalidationData::new, ownedFuture);
+    return ownedFuture;
   }
 
+  sealed interface GetDirectoryListingDependenciesResult
+      permits DirectoryListingDependencies, FutureDirectoryListingDependencies {}
+
   /**
-   * The main purpose of this class is to act as a {@link ListenableFuture<FileDependencies>}.
+   * The main purpose of this class is to act as a {@link
+   * ListenableFuture<DirectoryListingDependencies>}.
    *
    * <p>Its specific type is explicitly visible to clients to allow them to cleanly distinguish it
-   * as a permitted subtype of {@link GetDependenciesResult}.
+   * as a permitted subtype of {@link GetDirectoryListingDependenciesResult}.
    */
-  static final class FutureFileDependencies extends AbstractFuture<FileDependencies>
-      implements GetDependenciesResult {
-    /** Used to establish exactly-once ownership of this future with {@link #tryTakeOwnership}. */
-    @SuppressWarnings({"UnusedVariable", "FieldCanBeFinal"}) // set with OWNED_HANDLE
-    private boolean owned = false;
+  static final class FutureDirectoryListingDependencies
+      extends SettableFutureWithOwnership<DirectoryListingDependencies>
+      implements GetDirectoryListingDependenciesResult {}
 
-    private boolean isSet = false;
-
-    private boolean tryTakeOwnership() {
-      return OWNED_HANDLE.compareAndSet(this, false, true);
+  /**
+   * Deserializes the resolved directory listing information associated with {@code key}.
+   *
+   * @param key should be as described at {@link DirectoryListingInvalidationData}.
+   * @return either an immediate {@link DirectoryListingDependencies} instance or effectively a
+   *     {@link ListenableFuture<DirectoryListingDependencies>} instance.
+   */
+  GetDirectoryListingDependenciesResult getDirectoryListingDependencies(String key) {
+    FutureDirectoryListingDependencies ownedFuture;
+    switch (listingCache.get(key, unused -> new FutureDirectoryListingDependencies())) {
+      case DirectoryListingDependencies dependencies:
+        return dependencies;
+      case FutureDirectoryListingDependencies future:
+        if (!future.tryTakeOwnership()) {
+          return future; // Owned by another thread.
+        }
+        ownedFuture = future;
+        break;
     }
-
-    private void setFutureFiles(ListenableFuture<FileDependencies> files) {
-      checkState(setFuture(files), "already set %s", this);
-      isSet = true;
-    }
-
-    private void setIoException(IOException e) {
-      checkState(setException(e));
-      isSet = true;
-    }
-
-    private void verifySet() {
-      if (!isSet) {
-        checkState(
-            setException(
-                new IllegalStateException(
-                    "future was unexpectedly unset, look for unchecked exceptions in"
-                        + " FileDependencyDeserializer")));
-      }
-    }
-
-    private static final VarHandle OWNED_HANDLE;
-
-    static {
-      try {
-        OWNED_HANDLE =
-            MethodHandles.lookup()
-                .findVarHandle(FutureFileDependencies.class, "owned", boolean.class);
-      } catch (ReflectiveOperationException e) {
-        throw new ExceptionInInitializerError(e);
-      }
-    }
+    // `ownedFuture` is owned by this thread, which must complete its value.
+    fetchInvalidationData(key, WaitForListingInvalidationData::new, ownedFuture);
+    return ownedFuture;
   }
 
-  private class WaitForData implements AsyncFunction<byte[], FileDependencies> {
+  // ---------- Begin FileDependencies deserialization implementation ----------
+
+  private class WaitForFileInvalidationData implements AsyncFunction<byte[], FileDependencies> {
     private final String key;
 
-    private WaitForData(String key) {
+    private WaitForFileInvalidationData(String key) {
       this.key = key;
     }
 
@@ -280,7 +286,7 @@
       // Replaces the cache value with the completed value. The future is likely to become eligible
       // for GC shortly after the return below. Clients are expected to retain the meaningful
       // top-level values.
-      dependenciesCache.put(key, dependencies);
+      fileCache.put(key, dependencies);
       return immediateFuture(dependencies);
     }
 
@@ -406,6 +412,91 @@
     return !previousParent.startsWith(newParent);
   }
 
+  // ---------- Begin DirectoryListingDependencies deserialization implementation ----------
+
+  private class WaitForListingInvalidationData
+      implements AsyncFunction<byte[], DirectoryListingDependencies> {
+    private final String key;
+
+    private WaitForListingInvalidationData(String key) {
+      this.key = key;
+    }
+
+    @Override
+    public ListenableFuture<DirectoryListingDependencies> apply(byte[] bytes)
+        throws InvalidProtocolBufferException {
+      var data = DirectoryListingInvalidationData.parseFrom(bytes, getEmptyRegistry());
+      if (data.hasOverflowKey() && !data.getOverflowKey().equals(key)) {
+        return immediateFailedFuture(
+            new SerializationException(
+                String.format(
+                    "Non-matching overflow key. This is possible if there is a key fingerprint"
+                        + " collision. Expected %s got %s",
+                    key, data)));
+      }
+
+      int pathBegin = key.indexOf(DIRECTORY_KEY_DELIMITER) + 1;
+
+      String path = key.substring(pathBegin);
+      if (path.isEmpty()) {
+        return immediateFuture(createAndCacheListingDependencies(key, ROOT_FILE));
+      }
+
+      String fileKey =
+          computeCacheKey(
+              path, data.hasFileMtsv() ? data.getFileMtsv() : MTSV_SENTINEL, FILE_KEY_DELIMITER);
+      switch (getFileDependencies(fileKey)) {
+        case FileDependencies dependencies:
+          return immediateFuture(createAndCacheListingDependencies(key, dependencies));
+        case FutureFileDependencies future:
+          return Futures.transform(
+              future, new WaitForListingFileDependencies(key), directExecutor());
+      }
+    }
+  }
+
+  private class WaitForListingFileDependencies
+      implements Function<FileDependencies, DirectoryListingDependencies> {
+    private final String key;
+
+    private WaitForListingFileDependencies(String key) {
+      this.key = key;
+    }
+
+    @Override
+    public DirectoryListingDependencies apply(FileDependencies dependencies) {
+      return createAndCacheListingDependencies(key, dependencies);
+    }
+  }
+
+  private DirectoryListingDependencies createAndCacheListingDependencies(
+      String key, FileDependencies dependencies) {
+    var result = new DirectoryListingDependencies(dependencies);
+    listingCache.put(key, result);
+    return result;
+  }
+
+  // ---------- Begin shared helpers ----------
+
+  private <T, FutureT extends SettableFutureWithOwnership<T>> void fetchInvalidationData(
+      String key, Function<String, AsyncFunction<byte[], T>> waitFactory, FutureT ownedFuture) {
+    try {
+      ListenableFuture<byte[]> futureBytes;
+      try {
+        futureBytes = fingerprintValueService.get(getKeyBytes(key));
+      } catch (IOException e) {
+        ownedFuture.failWith(e);
+        return;
+      }
+
+      ownedFuture.completeWith(
+          Futures.transformAsync(
+              futureBytes, waitFactory.apply(key), fingerprintValueService.getExecutor()));
+    } finally {
+      ownedFuture.verifyComplete();
+    }
+  }
+
   private KeyBytesProvider getKeyBytes(String cacheKey) {
     if (cacheKey.length() > MAX_KEY_LENGTH) {
       return fingerprintValueService.fingerprint(cacheKey.getBytes(UTF_8));
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java
index ad0b618..cf99008 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java
@@ -194,7 +194,7 @@
       reference.populate(
           computeCacheKey(rootedPath.getRootRelativePath(), mtsv, DIRECTORY_KEY_DELIMITER));
     }
-    // If this is reached, this thread owns `reference` and must complete it's future.
+    // If this is reached, this thread owns `reference` and must complete its future.
     boolean writeStatusSet = false;
     try {
       DirectoryListingInvalidationData.Builder data = DirectoryListingInvalidationData.newBuilder();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileSystemDependencies.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileSystemDependencies.java
new file mode 100644
index 0000000..072e135
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileSystemDependencies.java
@@ -0,0 +1,25 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe.serialization.analysis;
+
+/**
+ * Union type for {@link FileDependencies} and {@link DirectoryListingDependencies}.
+ *
+ * <p>At a structural level these two classes are very similar and {@link
+ * DirectoryListingDependencies} could be modeled plainly as {@link FileDependencies}. The crucial
+ * difference is that {@link FileDependencies#containsMatch(ImmutableSet<String>)} takes a set of
+ * files and {@link DirectoryListingDependencies#matchesAnyDirectories(ImmutableSet<String>)} takes
+ * a set of directory names so the two types are deliberately separated.
+ */
+sealed interface FileSystemDependencies permits FileDependencies, DirectoryListingDependencies {}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/SettableFutureWithOwnership.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/SettableFutureWithOwnership.java
new file mode 100644
index 0000000..5d8efd3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/SettableFutureWithOwnership.java
@@ -0,0 +1,82 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe.serialization.analysis;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+
+/** A tailored settable future with ownership tracking. */
+abstract class SettableFutureWithOwnership<T> extends AbstractFuture<T> {
+  /** Used to establish exactly-once ownership of this future with {@link #tryTakeOwnership}. */
+  @SuppressWarnings({"UnusedVariable", "FieldCanBeFinal"}) // set with OWNED_HANDLE
+  private boolean owned = false;
+
+  private boolean isSet = false;
+
+  /**
+   * Returns true once.
+   *
+   * <p>When using {@link com.github.benmanes.caffeine.cache.Cache#get} with future values and a
+   * mapping function, there's a need to determine which thread owns the future. This method
+   * provides such a mechanism.
+   *
+   * <p>When this returns true, the caller must call either {@link #completeWith} or {@link
+   * #failWith}.
+   */
+  boolean tryTakeOwnership() {
+    return OWNED_HANDLE.compareAndSet(this, false, true);
+  }
+
+  void completeWith(ListenableFuture<T> future) {
+    checkState(setFuture(future), "already set %s", this);
+    isSet = true;
+  }
+
+  void failWith(IOException e) {
+    checkState(setException(e));
+    isSet = true;
+  }
+
+  /**
+   * Verifies that the future was complete.
+   *
+   * <p>With settable futures, there's a risk of deadlock-like behavior if the future is not
+   * complete. The owning thread should call this in a finally clause to fail-fast instead.
+   */
+  void verifyComplete() {
+    if (!isSet) {
+      checkState(
+          setException(
+              new IllegalStateException(
+                  "future was unexpectedly unset, look for unchecked exceptions")));
+    }
+  }
+
+  private static final VarHandle OWNED_HANDLE;
+
+  static {
+    try {
+      OWNED_HANDLE =
+          MethodHandles.lookup()
+              .findVarHandle(SettableFutureWithOwnership.class, "owned", boolean.class);
+    } catch (ReflectiveOperationException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+}