Refactor include scanner API to use futures

Note that the implementation is still mostly synchronous.

Progress on #6394.

PiperOrigin-RevId: 245374534
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/CppIncludeScanningContextImpl.java b/src/main/java/com/google/devtools/build/lib/includescanning/CppIncludeScanningContextImpl.java
index 6652324..b611522 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/CppIncludeScanningContextImpl.java
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/CppIncludeScanningContextImpl.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.includescanning;
 
 import com.google.common.base.Supplier;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ExecException;
@@ -38,7 +39,7 @@
   }
 
   @Override
-  public Iterable<Artifact> findAdditionalInputs(
+  public ListenableFuture<Iterable<Artifact>> findAdditionalInputs(
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
       IncludeProcessing includeProcessing,
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/IncludeScanningModule.java b/src/main/java/com/google/devtools/build/lib/includescanning/IncludeScanningModule.java
index 305d06c..cd4ff72 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/IncludeScanningModule.java
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/IncludeScanningModule.java
@@ -15,6 +15,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Supplier;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -47,6 +48,7 @@
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
 import com.google.devtools.build.lib.skyframe.MutableSupplier;
+import com.google.devtools.build.lib.vfs.IORuntimeException;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunction;
 import com.google.devtools.build.skyframe.SkyFunctionName;
@@ -56,6 +58,7 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.logging.Logger;
 
@@ -180,18 +183,30 @@
       for (Artifact path : legalOutputPaths) {
         pathToLegalOutputArtifact.put(path.getExecPath(), path);
       }
-      scanner.process(
-          source,
-          ImmutableList.of(source),
-          // For Swig include scanning just point to the output file in the map.
-          new IncludeScanningHeaderData.Builder(
-                  pathToLegalOutputArtifact.build(), /*modularHeaders=*/ ImmutableSet.of())
-              .build(),
-          ImmutableList.of(),
-          includes,
-          actionExecutionMetadata,
-          actionExecContext,
-          grepIncludes);
+      try {
+        scanner
+            .processAsync(
+                source,
+                ImmutableList.of(source),
+                // For Swig include scanning just point to the output file in the map.
+                new IncludeScanningHeaderData.Builder(
+                        pathToLegalOutputArtifact.build(), /*modularHeaders=*/ ImmutableSet.of())
+                    .build(),
+                ImmutableList.of(),
+                includes,
+                actionExecutionMetadata,
+                actionExecContext,
+                grepIncludes)
+            .get();
+      } catch (ExecutionException e) {
+        Throwables.throwIfInstanceOf(e.getCause(), ExecException.class);
+        Throwables.throwIfInstanceOf(e.getCause(), InterruptedException.class);
+        if (e.getCause() instanceof IORuntimeException) {
+          throw ((IORuntimeException) e.getCause()).getCauseIOException();
+        }
+        Throwables.throwIfUnchecked(e.getCause());
+        throw new IllegalStateException(e.getCause());
+      }
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/includescanning/LegacyIncludeScanner.java b/src/main/java/com/google/devtools/build/lib/includescanning/LegacyIncludeScanner.java
index b37676d..d14bbb2 100644
--- a/src/main/java/com/google/devtools/build/lib/includescanning/LegacyIncludeScanner.java
+++ b/src/main/java/com/google/devtools/build/lib/includescanning/LegacyIncludeScanner.java
@@ -18,6 +18,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
 import com.google.devtools.build.lib.actions.Artifact;
@@ -417,7 +419,7 @@
   }
 
   @Override
-  public void process(
+  public ListenableFuture<?> processAsync(
       Artifact mainSource,
       Collection<Artifact> sources,
       IncludeScanningHeaderData includeScanningHeaderData,
@@ -427,140 +429,34 @@
       ActionExecutionContext actionExecutionContext,
       Artifact grepIncludes)
       throws IOException, ExecException, InterruptedException {
-    try {
-      // Because our IncludeVisitor can dynamically switch to in-thread execution, we could get
-      // unlucky and run one of the async calls on the main thread. Catch and rethrow the runtime
-      // exceptions as the appropriate corresponding checked exception.
-      processInternal(
-          mainSource,
-          sources,
-          includeScanningHeaderData,
-          cmdlineIncludes,
-          includes,
-          actionExecutionMetadata,
-          actionExecutionContext,
-          grepIncludes);
-    } catch (IORuntimeException e) {
-      throw e.getCauseIOException();
-    } catch (ExecRuntimeException e) {
-      throw e.getRealCause();
-    } catch (InterruptedRuntimeException e) {
-      throw e.getRealCause();
-    }
+    ImmutableSet<Artifact> pathHints =
+        prepare(actionExecutionContext.getEnvironmentForDiscoveringInputs());
+    IncludeVisitor visitor = new IncludeVisitor(includeScanningHeaderData.getModularHeaders());
+    return visitor.processInternal(
+        mainSource,
+        sources,
+        includeScanningHeaderData,
+        cmdlineIncludes,
+        includes,
+        actionExecutionMetadata,
+        actionExecutionContext,
+        grepIncludes,
+        pathHints);
   }
 
-  private void processInternal(
-      Artifact mainSource,
-      Collection<Artifact> sources,
-      IncludeScanningHeaderData includeScanningHeaderData,
-      List<String> cmdlineIncludes,
-      Set<Artifact> includes,
-      ActionExecutionMetadata actionExecutionMetadata,
-      ActionExecutionContext actionExecutionContext,
-      Artifact grepIncludes)
-      throws IOException, ExecException, InterruptedException {
-    Preconditions.checkArgument(mainSource == null || sources.contains(mainSource),
-        "The main source '%s' is not part of '%s'", mainSource, sources);
-    ImmutableSet.Builder<Artifact> pathHints = null;
-    SkyFunction.Environment env = actionExecutionContext.getEnvironmentForDiscoveringInputs();
+  private ImmutableSet<Artifact> prepare(SkyFunction.Environment env) throws InterruptedException {
     if (parser.getHints() != null) {
-      pathHints = ImmutableSet.builderWithExpectedSize(quoteIncludePaths.size());
       Collection<Artifact> artifacts =
           parser.getHints().getPathLevelHintedInclusions(quoteIncludePaths, env);
-      if (!env.valuesMissing()) {
-        pathHints.addAll(Preconditions.checkNotNull(artifacts, quoteIncludePaths));
+      if (env.valuesMissing()) {
+        throw new MissingDepException();
       }
+      ImmutableSet.Builder<Artifact> pathHints;
+      pathHints = ImmutableSet.builderWithExpectedSize(quoteIncludePaths.size());
+      pathHints.addAll(Preconditions.checkNotNull(artifacts, quoteIncludePaths));
+      return pathHints.build();
     }
-    if (env.valuesMissing()) {
-      throw new MissingDepException();
-    }
-
-    Set<ArtifactWithInclusionContext> visitedInclusions = Sets.newConcurrentHashSet();
-
-    IncludeVisitor visitor = new IncludeVisitor(includeScanningHeaderData.getModularHeaders());
-
-    try {
-      // Process cmd line includes, if specified.
-      if (mainSource != null && !cmdlineIncludes.isEmpty()) {
-        visitor.processCmdlineIncludes(
-            mainSource,
-            cmdlineIncludes,
-            grepIncludes,
-            includes,
-            includeScanningHeaderData.getPathToLegalOutputArtifact(),
-            actionExecutionMetadata,
-            actionExecutionContext,
-            visitedInclusions);
-        visitor.sync();
-      }
-
-      visitor.processBulkAsync(
-          sources,
-          includes,
-          includeScanningHeaderData.getPathToLegalOutputArtifact(),
-          actionExecutionMetadata,
-          actionExecutionContext,
-          visitedInclusions,
-          grepIncludes);
-      visitor.sync();
-
-      // Process include hints
-      // TODO(ulfjack): Make this code go away. Use the new hinted inclusions instead.
-      Hints hints = parser.getHints();
-      if (hints != null) {
-        // Follow "path" hints.
-        visitor.processBulkAsync(
-            pathHints.build(),
-            includes,
-            includeScanningHeaderData.getPathToLegalOutputArtifact(),
-            actionExecutionMetadata,
-            actionExecutionContext,
-            visitedInclusions,
-            grepIncludes);
-        // Follow "file" hints for the primary sources.
-        for (Artifact source : sources) {
-          visitor.processFileLevelHintsAsync(
-              hints,
-              source,
-              includes,
-              includeScanningHeaderData.getPathToLegalOutputArtifact(),
-              actionExecutionMetadata,
-              actionExecutionContext,
-              visitedInclusions,
-              grepIncludes);
-        }
-        visitor.sync();
-
-        // Follow "file" hints for all included headers, transitively.
-        Set<Artifact> frontier = includes;
-        while (!frontier.isEmpty()) {
-          Set<Artifact> adjacent = Sets.newConcurrentHashSet();
-          for (Artifact include : frontier) {
-            visitor.processFileLevelHintsAsync(
-                hints,
-                include,
-                adjacent,
-                includeScanningHeaderData.getPathToLegalOutputArtifact(),
-                actionExecutionMetadata,
-                actionExecutionContext,
-                visitedInclusions,
-                grepIncludes);
-          }
-          visitor.sync();
-          // Keep novel nodes as the next frontier.
-          for (Iterator<Artifact> iter = adjacent.iterator(); iter.hasNext(); ) {
-            if (!includes.add(iter.next())) {
-              iter.remove();
-            }
-          }
-          frontier = adjacent;
-        }
-      }
-    } catch (IOException | InterruptedException | ExecException | MissingDepException e) {
-      // Careful: Do not leak visitation threads if we have an exception in the initial thread.
-      visitor.sync();
-      throw e;
-    }
+    return ImmutableSet.of();
   }
 
   private static void checkForInterrupt(String operation, Object source)
@@ -590,8 +486,8 @@
   }
 
   /**
-   * Implements a potentially parallel traversal over source files using a
-   * thread pool shared across different IncludeScanner instances.
+   * Implements a potentially parallel traversal over source files using a thread pool shared across
+   * different IncludeScanner instances.
    */
   private class IncludeVisitor extends AbstractQueueVisitor {
     /** The set of headers known to be part of a C++ module. Scanning can stop here. */
@@ -606,10 +502,106 @@
       this.modularHeaders = modularHeaders;
     }
 
-    /**
-     * Block for the completion of all outstanding visitations.
-     */
-    public void sync() throws IOException, ExecException, InterruptedException {
+    ListenableFuture<?> processInternal(
+        Artifact mainSource,
+        Collection<Artifact> sources,
+        IncludeScanningHeaderData includeScanningHeaderData,
+        List<String> cmdlineIncludes,
+        Set<Artifact> includes,
+        ActionExecutionMetadata actionExecutionMetadata,
+        ActionExecutionContext actionExecutionContext,
+        Artifact grepIncludes,
+        ImmutableSet<Artifact> pathHints)
+        throws InterruptedException, IOException, ExecException {
+      Set<ArtifactWithInclusionContext> visitedInclusions = Sets.newConcurrentHashSet();
+
+      try {
+        // Process cmd line includes, if specified.
+        if (mainSource != null && !cmdlineIncludes.isEmpty()) {
+          processCmdlineIncludes(
+              mainSource,
+              cmdlineIncludes,
+              grepIncludes,
+              includes,
+              includeScanningHeaderData.getPathToLegalOutputArtifact(),
+              actionExecutionMetadata,
+              actionExecutionContext,
+              visitedInclusions);
+          sync();
+        }
+
+        processBulkAsync(
+            sources,
+            includes,
+            includeScanningHeaderData.getPathToLegalOutputArtifact(),
+            actionExecutionMetadata,
+            actionExecutionContext,
+            visitedInclusions,
+            grepIncludes);
+        sync();
+
+        // Process include hints
+        // TODO(ulfjack): Make this code go away. Use the new hinted inclusions instead.
+        Hints hints = parser.getHints();
+        if (hints != null) {
+          // Follow "path" hints.
+          processBulkAsync(
+              pathHints,
+              includes,
+              includeScanningHeaderData.getPathToLegalOutputArtifact(),
+              actionExecutionMetadata,
+              actionExecutionContext,
+              visitedInclusions,
+              grepIncludes);
+          // Follow "file" hints for the primary sources.
+          for (Artifact source : sources) {
+            processFileLevelHintsAsync(
+                hints,
+                source,
+                includes,
+                includeScanningHeaderData.getPathToLegalOutputArtifact(),
+                actionExecutionMetadata,
+                actionExecutionContext,
+                visitedInclusions,
+                grepIncludes);
+          }
+          sync();
+
+          // Follow "file" hints for all included headers, transitively.
+          Set<Artifact> frontier = includes;
+          while (!frontier.isEmpty()) {
+            Set<Artifact> adjacent = Sets.newConcurrentHashSet();
+            for (Artifact include : frontier) {
+              processFileLevelHintsAsync(
+                  hints,
+                  include,
+                  adjacent,
+                  includeScanningHeaderData.getPathToLegalOutputArtifact(),
+                  actionExecutionMetadata,
+                  actionExecutionContext,
+                  visitedInclusions,
+                  grepIncludes);
+            }
+            sync();
+            // Keep novel nodes as the next frontier.
+            for (Iterator<Artifact> iter = adjacent.iterator(); iter.hasNext(); ) {
+              if (!includes.add(iter.next())) {
+                iter.remove();
+              }
+            }
+            frontier = adjacent;
+          }
+        }
+      } catch (IOException | InterruptedException | ExecException | MissingDepException e) {
+        // Careful: Do not leak visitation threads if we have an exception in the initial thread.
+        sync();
+        throw e;
+      }
+      return Futures.immediateFuture(null);
+    }
+
+    /** Block for the completion of all outstanding visitations. */
+    private void sync() throws IOException, ExecException, InterruptedException {
       try {
         super.awaitQuiescence(true);
       } catch (InterruptedException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
index 047587d..73e73d4 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileAction.java
@@ -77,6 +77,7 @@
 import com.google.devtools.build.lib.util.ShellEscaper;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.IORuntimeException;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
@@ -97,6 +98,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
 import javax.annotation.Nullable;
 
 /** Action that represents some kind of C++ compilation step. */
@@ -401,28 +403,37 @@
   private Iterable<Artifact> findUsedHeaders(
       ActionExecutionContext actionExecutionContext, IncludeScanningHeaderData headerData)
       throws ActionExecutionException, InterruptedException {
-    Iterable<Artifact> includeScanningResult;
+    if (!shouldScanIncludes()) {
+      return NestedSetBuilder.fromNestedSet(ccCompilationContext.getDeclaredIncludeSrcs())
+          .addTransitive(additionalPrunableHeaders)
+          .build();
+    }
     try {
-      includeScanningResult =
-          actionExecutionContext
-              .getContext(CppIncludeScanningContext.class)
-              .findAdditionalInputs(
-                  this,
-                  actionExecutionContext,
-                  includeProcessing,
-                  headerData);
+      try {
+        return actionExecutionContext
+            .getContext(CppIncludeScanningContext.class)
+            .findAdditionalInputs(this, actionExecutionContext, includeProcessing, headerData)
+            .get();
+      } catch (ExecutionException e) {
+        Throwables.throwIfInstanceOf(e.getCause(), ExecException.class);
+        Throwables.throwIfInstanceOf(e.getCause(), InterruptedException.class);
+        if (e.getCause() instanceof IORuntimeException) {
+          throw new EnvironmentalExecException(
+              ((IORuntimeException) e.getCause()).getCauseIOException().getMessage(),
+              ((IORuntimeException) e.getCause()).getCauseIOException());
+        }
+        if (e.getCause() instanceof IOException) {
+          throw new EnvironmentalExecException(e.getMessage(), e);
+        }
+        Throwables.throwIfUnchecked(e.getCause());
+        throw new IllegalStateException(e.getCause());
+      }
     } catch (ExecException e) {
       throw e.toActionExecutionException(
           "Include scanning of rule '" + getOwner().getLabel() + "'",
           actionExecutionContext.getVerboseFailures(),
           this);
     }
-    if (includeScanningResult != null) {
-      return includeScanningResult;
-    }
-    return NestedSetBuilder.fromNestedSet(ccCompilationContext.getDeclaredIncludeSrcs())
-        .addTransitive(additionalPrunableHeaders)
-        .build();
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppIncludeScanningContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppIncludeScanningContext.java
index 72f842e..21e5d38 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppIncludeScanningContext.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppIncludeScanningContext.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.rules.cpp;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionContext;
 import com.google.devtools.build.lib.actions.ActionContextMarker;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
@@ -33,7 +34,7 @@
    * <p>Returns null if additional inputs will only be found during action execution, not before.
    */
   @Nullable
-  Iterable<Artifact> findAdditionalInputs(
+  ListenableFuture<Iterable<Artifact>> findAdditionalInputs(
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
       IncludeProcessing includeProcessing,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProcessing.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProcessing.java
index 0a85187..0843be9 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProcessing.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeProcessing.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.lib.rules.cpp;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ExecException;
@@ -24,7 +25,7 @@
 /** Used as an interface to thin header inputs to compile actions for C++-like compiles. */
 public interface IncludeProcessing {
   /** Performs include processing actions and returns the processed set of resulting headers. */
-  Iterable<Artifact> determineAdditionalInputs(
+  ListenableFuture<Iterable<Artifact>> determineAdditionalInputs(
       @Nullable IncludeScannerSupplier includeScannerSupplier,
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java
index e5d83d5..d81b6d2 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanner.java
@@ -15,22 +15,14 @@
 package com.google.devtools.build.lib.rules.cpp;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
 import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
-import com.google.devtools.build.lib.actions.UserExecException;
-import com.google.devtools.build.lib.profiler.Profiler;
-import com.google.devtools.build.lib.profiler.ProfilerTask;
-import com.google.devtools.build.lib.profiler.SilentCloseable;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -49,23 +41,24 @@
    * impact in the case that we are scanning a single source file, since it is already known to be
    * an input. However, this is necessary when we have more than one source to scan from, for
    * example when building C++ modules. In that case we have one of two possibilities:
+   *
    * <ol>
-   * <li>We compile a header module - there, the .cppmap file is the main source file (which we do
-   *     not include-scan, as that would require an extra parser), and thus already in the input;
-   *     all headers in the .cppmap file are our entry points for include scanning, but are not yet
-   *     in the inputs - they get added here.</li>
-   * <li>We compile an object file that uses a header module; currently using a header module
-   *     requires all headers it can reference to be available for the compilation. The header
-   *     module can reference headers that are not in the transitive include closure of the current
-   *     translation unit. Therefore, {@link CppCompileAction} adds all headers specified
-   *     transitively for compiled header modules as include scanning entry points, and we need to
-   *     add the entry points to the inputs here.</li></ol>
-   * </p>
+   *   <li>We compile a header module - there, the .cppmap file is the main source file (which we do
+   *       not include-scan, as that would require an extra parser), and thus already in the input;
+   *       all headers in the .cppmap file are our entry points for include scanning, but are not
+   *       yet in the inputs - they get added here.
+   *   <li>We compile an object file that uses a header module; currently using a header module
+   *       requires all headers it can reference to be available for the compilation. The header
+   *       module can reference headers that are not in the transitive include closure of the
+   *       current translation unit. Therefore, {@link CppCompileAction} adds all headers specified
+   *       transitively for compiled header modules as include scanning entry points, and we need to
+   *       add the entry points to the inputs here.
+   * </ol>
    *
    * <p>{@code mainSource} is the source file relative to which the {@code cmdlineIncludes} are
-   * interpreted.</p>
+   * interpreted.
    */
-  void process(
+  ListenableFuture<?> processAsync(
       Artifact mainSource,
       Collection<Artifact> sources,
       IncludeScanningHeaderData includeScanningHeaderData,
@@ -87,104 +80,13 @@
   }
 
   /**
-   * Helper class that exists just to provide a static method that prepares the arguments with which
-   * to call an IncludeScanner.
-   */
-  class IncludeScanningPreparer {
-    private IncludeScanningPreparer() {}
-
-    /**
-     * Returns the files transitively included by the source files of the given IncludeScannable.
-     *
-     * @param action IncludeScannable whose sources' transitive includes will be returned.
-     * @param includeScannerSupplier supplies IncludeScanners to actually do the transitive
-     *                               scanning (and caching results) for a given source file.
-     * @param actionExecutionContext the context for {@code action}.
-     * @param profilerTaskName what the {@link Profiler} should record this call for.
-     */
-    public static Collection<Artifact> scanForIncludedInputs(
-        IncludeScannable action,
-        IncludeScannerSupplier includeScannerSupplier,
-        IncludeScanningHeaderData includeScanningHeaderData,
-        ActionExecutionMetadata actionExecutionMetadata,
-        ActionExecutionContext actionExecutionContext,
-        String profilerTaskName)
-        throws ExecException, InterruptedException {
-
-      Set<Artifact> includes = Sets.newConcurrentHashSet();
-
-      final List<PathFragment> absoluteBuiltInIncludeDirs = new ArrayList<>();
-      includes.addAll(action.getBuiltInIncludeFiles());
-
-      Profiler profiler = Profiler.instance();
-      try (SilentCloseable c = profiler.profile(ProfilerTask.SCANNER, profilerTaskName)) {
-        // Deduplicate include directories. This can occur especially with "built-in" and "system"
-        // include directories because of the way we retrieve them. Duplicate include directories
-        // really mess up #include_next directives.
-        Set<PathFragment> includeDirs = new LinkedHashSet<>(action.getIncludeDirs());
-        List<PathFragment> quoteIncludeDirs = action.getQuoteIncludeDirs();
-        List<String> cmdlineIncludes = includeScanningHeaderData.getCmdlineIncludes();
-
-        includeDirs.addAll(includeScanningHeaderData.getSystemIncludeDirs());
-
-        // Add the system include paths to the list of include paths.
-        for (PathFragment pathFragment : action.getBuiltInIncludeDirectories()) {
-          if (pathFragment.isAbsolute()) {
-            absoluteBuiltInIncludeDirs.add(pathFragment);
-          }
-          includeDirs.add(pathFragment);
-        }
-
-        List<PathFragment> includeDirList = ImmutableList.copyOf(includeDirs);
-        IncludeScanner scanner =
-            includeScannerSupplier.scannerFor(quoteIncludeDirs, includeDirList);
-
-        Artifact mainSource = action.getMainIncludeScannerSource();
-        Collection<Artifact> sources = action.getIncludeScannerSources();
-        scanner.process(
-            mainSource,
-            sources,
-            includeScanningHeaderData,
-            cmdlineIncludes,
-            includes,
-            actionExecutionMetadata,
-            actionExecutionContext,
-            action.getGrepIncludes());
-
-      } catch (IOException e) {
-        throw new EnvironmentalExecException(e.getMessage());
-      }
-
-      // Collect inputs and output
-      List<Artifact> inputs = new ArrayList<>();
-      for (Artifact included : includes) {
-        // Check for absolute includes -- we assign the file system root as
-        // the root path for such includes
-        if (included.getRoot().getRoot().isAbsolute()) {
-          if (FileSystemUtils.startsWithAny(
-              actionExecutionContext.getInputPath(included).asFragment(),
-              absoluteBuiltInIncludeDirs)) {
-            // Skip include files found in absolute include directories.
-            continue;
-          }
-          throw new UserExecException(
-              "illegal absolute path to include file: "
-                  + actionExecutionContext.getInputPath(included));
-        }
-        inputs.add(included);
-      }
-      return inputs;
-    }
-  }
-
-  /**
    * Holds pre-aggregated information that the {@link IncludeScanner} needs from the compilation
    * action.
    */
   class IncludeScanningHeaderData {
     /**
      * Lookup table to find the {@link Artifact}s of generated files based on their {@link
-     * Artifact#execPath}.
+     * Artifact#getExecPath}.
      */
     private final Map<PathFragment, Artifact> pathToLegalOutputArtifact;
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
index 9e68607..61dc2ee 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
@@ -15,21 +15,41 @@
 package com.google.devtools.build.lib.rules.cpp;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.profiler.SilentCloseable;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScannerSupplier;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScanningHeaderData;
-import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScanningPreparer;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
 import javax.annotation.Nullable;
 
 /** Runs include scanning on actions, if include scanning is enabled. */
 public class IncludeScanning implements IncludeProcessing {
+
   @AutoCodec public static final IncludeScanning INSTANCE = new IncludeScanning();
 
+  @Nullable
   @Override
-  public @Nullable Iterable<Artifact> determineAdditionalInputs(
+  public ListenableFuture<Iterable<Artifact>> determineAdditionalInputs(
       @Nullable IncludeScannerSupplier includeScannerSupplier,
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
@@ -41,12 +61,87 @@
       return null;
     }
 
-    return IncludeScanningPreparer.scanForIncludedInputs(
-        action,
-        Preconditions.checkNotNull(includeScannerSupplier, action),
-        includeScanningHeaderData,
-        action,
-        actionExecutionContext,
-        action.getSourceFile().getExecPathString());
+    Preconditions.checkNotNull(includeScannerSupplier, action);
+
+    Set<Artifact> includes = Sets.newConcurrentHashSet();
+
+    final List<PathFragment> absoluteBuiltInIncludeDirs = new ArrayList<>();
+    includes.addAll(action.getBuiltInIncludeFiles());
+
+    // Deduplicate include directories. This can occur especially with "built-in" and "system"
+    // include directories because of the way we retrieve them. Duplicate include directories
+    // really mess up #include_next directives.
+    Set<PathFragment> includeDirs = new LinkedHashSet<>(action.getIncludeDirs());
+    List<PathFragment> quoteIncludeDirs = action.getQuoteIncludeDirs();
+    List<String> cmdlineIncludes = includeScanningHeaderData.getCmdlineIncludes();
+
+    includeDirs.addAll(includeScanningHeaderData.getSystemIncludeDirs());
+
+    // Add the system include paths to the list of include paths.
+    for (PathFragment pathFragment : action.getBuiltInIncludeDirectories()) {
+      if (pathFragment.isAbsolute()) {
+        absoluteBuiltInIncludeDirs.add(pathFragment);
+      }
+      includeDirs.add(pathFragment);
+    }
+
+    List<PathFragment> includeDirList = ImmutableList.copyOf(includeDirs);
+    IncludeScanner scanner = includeScannerSupplier.scannerFor(quoteIncludeDirs, includeDirList);
+
+    Artifact mainSource = action.getMainIncludeScannerSource();
+    Collection<Artifact> sources = action.getIncludeScannerSources();
+
+    Profiler profiler = Profiler.instance();
+    try (SilentCloseable c =
+        profiler.profile(ProfilerTask.SCANNER, action.getSourceFile().getExecPathString())) {
+      ListenableFuture<?> future =
+          scanner.processAsync(
+              mainSource,
+              sources,
+              includeScanningHeaderData,
+              cmdlineIncludes,
+              includes,
+              action,
+              actionExecutionContext,
+              action.getGrepIncludes());
+      return Futures.transformAsync(
+          future,
+          new AsyncFunction<Object, Iterable<Artifact>>() {
+            @Override
+            public ListenableFuture<Iterable<Artifact>> apply(Object input) throws Exception {
+              return Futures.immediateFuture(
+                  collect(actionExecutionContext, includes, absoluteBuiltInIncludeDirs));
+            }
+          },
+          MoreExecutors.directExecutor());
+    } catch (IOException e) {
+      throw new EnvironmentalExecException("I/O failure during include scanning", e);
+    }
+  }
+
+  private static List<Artifact> collect(
+      ActionExecutionContext actionExecutionContext,
+      Set<Artifact> includes,
+      List<PathFragment> absoluteBuiltInIncludeDirs)
+      throws ExecException {
+    // Collect inputs and output
+    List<Artifact> inputs = new ArrayList<>();
+    for (Artifact included : includes) {
+      // Check for absolute includes -- we assign the file system root as
+      // the root path for such includes
+      if (included.getRoot().getRoot().isAbsolute()) {
+        if (FileSystemUtils.startsWithAny(
+            actionExecutionContext.getInputPath(included).asFragment(),
+            absoluteBuiltInIncludeDirs)) {
+          // Skip include files found in absolute include directories.
+          continue;
+        }
+        throw new UserExecException(
+            "illegal absolute path to include file: "
+                + actionExecutionContext.getInputPath(included));
+      }
+      inputs.add(included);
+    }
+    return inputs;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/NoProcessing.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/NoProcessing.java
index 9946c52..07e08e3 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/NoProcessing.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/NoProcessing.java
@@ -14,9 +14,9 @@
 
 package com.google.devtools.build.lib.rules.cpp;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScannerSupplier;
 import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScanningHeaderData;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
@@ -27,12 +27,11 @@
   @AutoCodec public static final NoProcessing INSTANCE = new NoProcessing();
 
   @Override
-  public Iterable<Artifact> determineAdditionalInputs(
+  public ListenableFuture<Iterable<Artifact>> determineAdditionalInputs(
       @Nullable IncludeScannerSupplier includeScannerSupplier,
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
-      IncludeScanningHeaderData includeScanningHeaderData)
-      throws ExecException, InterruptedException {
+      IncludeScanningHeaderData includeScanningHeaderData) {
     return null;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/HeaderThinning.java b/src/main/java/com/google/devtools/build/lib/rules/objc/HeaderThinning.java
index c5d3ecb..95c32ea 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/HeaderThinning.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/HeaderThinning.java
@@ -16,6 +16,8 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.ArtifactPathResolver;
@@ -74,7 +76,7 @@
   }
 
   @Override
-  public Iterable<Artifact> determineAdditionalInputs(
+  public ListenableFuture<Iterable<Artifact>> determineAdditionalInputs(
       @Nullable IncludeScannerSupplier includeScannerSupplier,
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
@@ -84,10 +86,14 @@
     if (headersListFile == null) {
       return null;
     }
-    return findRequiredHeaderInputs(action.getSourceFile(), headersListFile, getAllowedInputsMap(),
-        actionExecutionContext == null
-            ? ArtifactPathResolver.IDENTITY
-            : actionExecutionContext.getPathResolver());
+    return Futures.immediateFuture(
+        findRequiredHeaderInputs(
+            action.getSourceFile(),
+            headersListFile,
+            getAllowedInputsMap(),
+            actionExecutionContext == null
+                ? ArtifactPathResolver.IDENTITY
+                : actionExecutionContext.getPathResolver()));
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/DummyCppIncludeScanningContext.java b/src/main/java/com/google/devtools/build/lib/standalone/DummyCppIncludeScanningContext.java
index d574027..656f826 100644
--- a/src/main/java/com/google/devtools/build/lib/standalone/DummyCppIncludeScanningContext.java
+++ b/src/main/java/com/google/devtools/build/lib/standalone/DummyCppIncludeScanningContext.java
@@ -13,10 +13,9 @@
 // limitations under the License.
 package com.google.devtools.build.lib.standalone;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
-import com.google.devtools.build.lib.actions.ActionExecutionException;
 import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ExecutionStrategy;
 import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
 import com.google.devtools.build.lib.rules.cpp.CppIncludeScanningContext;
@@ -29,12 +28,11 @@
 class DummyCppIncludeScanningContext implements CppIncludeScanningContext {
   @Override
   @Nullable
-  public Iterable<Artifact> findAdditionalInputs(
+  public ListenableFuture<Iterable<Artifact>> findAdditionalInputs(
       CppCompileAction action,
       ActionExecutionContext actionExecutionContext,
       IncludeProcessing includeProcessing,
-      IncludeScanningHeaderData includeScanningHeaderData)
-      throws ExecException, InterruptedException, ActionExecutionException {
+      IncludeScanningHeaderData includeScanningHeaderData) {
     return null;
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
index 63a4c13..861990c 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -34,10 +35,13 @@
 import com.google.devtools.build.lib.packages.util.MockObjcSupport;
 import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.IORuntimeException;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -61,6 +65,22 @@
         "--ios_sdk_version=" + MockObjcSupport.DEFAULT_IOS_SDK_VERSION);
   }
 
+  private Iterable<Artifact> determineAdditionalInputs(
+      HeaderThinning headerThinning, CppCompileAction action)
+      throws ExecException, InterruptedException, IOException {
+    try {
+      return headerThinning.determineAdditionalInputs(null, action, null, null).get();
+    } catch (ExecutionException e) {
+      Throwables.throwIfInstanceOf(e.getCause(), ExecException.class);
+      Throwables.throwIfInstanceOf(e.getCause(), InterruptedException.class);
+      if (e.getCause() instanceof IORuntimeException) {
+        throw ((IORuntimeException) e.getCause()).getCauseIOException();
+      }
+      Throwables.throwIfUnchecked(e.getCause());
+      throw new IllegalStateException(e.getCause());
+    }
+  }
+
   @Test
   public void testCppCompileActionHeaderThinningCanDetermineAdditionalInputs() throws Exception {
     Artifact sourceFile = getSourceArtifact("objc/a.m");
@@ -74,8 +94,7 @@
     HeaderThinning headerThinning = new HeaderThinning(getPotentialHeaders(expectedHeaders));
     writeToHeadersListFile(action, "objc/a.pch", "objc/b.h", "objc/c", "objc/d.hpp");
 
-    Iterable<Artifact> headersFound =
-        headerThinning.determineAdditionalInputs(null, action, null, null);
+    Iterable<Artifact> headersFound = determineAdditionalInputs(headerThinning, action);
     assertThat(headersFound).containsExactlyElementsIn(expectedHeaders);
   }
 
@@ -89,7 +108,7 @@
     writeToHeadersListFile(action, "objc/a.h", "objc/b.h", "objc/c.h");
 
     try {
-      headerThinning.determineAdditionalInputs(null, action, null, null);
+      determineAdditionalInputs(headerThinning, action);
       fail("Exception was not thrown");
     } catch (ExecException e) {
       assertThat(e).hasMessageThat().containsMatch("(objc/c.h)");
@@ -106,8 +125,7 @@
     HeaderThinning headerThinning = new HeaderThinning(getPotentialHeaders(expectedHeaders));
     writeToHeadersListFile(action, "objc/a.h", "tree/dir/c.h");
 
-    Iterable<Artifact> headersFound =
-        headerThinning.determineAdditionalInputs(null, action, null, null);
+    Iterable<Artifact> headersFound = determineAdditionalInputs(headerThinning, action);
     assertThat(headersFound).containsExactlyElementsIn(expectedHeaders);
   }