Implement RemoteDownloader w/ `--experimental_remote_downloader`

This is the Bazel client implementation of https://github.com/bazelbuild/proposals/pull/160. It allows downloading of external dependencies to be delegated to a remote service.

TODOs:
- [x] Once https://github.com/bazelbuild/remote-apis/pull/112 is merged, the vendored copy of `bazelbuild/remote-apis` should be updated. I've used a [WIP] placeholder for now.
- [x] If the general approach looks reasonable then I'll add tests. Currently I've been testing with an in-house implementation of the downloader server.

R: @buchgr @dslomov
CC: @EricBurnett @sstriker @ulfjack

Closes #10622.

PiperOrigin-RevId: 300116716
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index f077ea8..f5d38c2 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -1252,6 +1252,7 @@
         ":util",
         "//src/main/java/com/google/devtools/build/lib/actions",
         "//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream/transports",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index 185193f..8212e47 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions;
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.RepositoryOverride;
 import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
+import com.google.devtools.build.lib.bazel.repository.downloader.DelegatingDownloader;
 import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
 import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
 import com.google.devtools.build.lib.bazel.repository.skylark.SkylarkRepositoryFunction;
@@ -97,8 +98,10 @@
   private final SkylarkRepositoryFunction skylarkRepositoryFunction;
   private final RepositoryCache repositoryCache = new RepositoryCache();
   private final HttpDownloader httpDownloader = new HttpDownloader();
+  private final DelegatingDownloader delegatingDownloader =
+      new DelegatingDownloader(httpDownloader);
   private final DownloadManager downloadManager =
-      new DownloadManager(repositoryCache, httpDownloader);
+      new DownloadManager(repositoryCache, delegatingDownloader);
   private final MutableSupplier<Map<String, String>> clientEnvironmentSupplier =
       new MutableSupplier<>();
   private ImmutableMap<RepositoryName, PathFragment> overrides = ImmutableMap.of();
@@ -334,6 +337,7 @@
         remoteExecutor = remoteExecutorFactory.create();
       }
       skylarkRepositoryFunction.setRepositoryRemoteExecutor(remoteExecutor);
+      delegatingDownloader.setDelegate(env.getRuntime().getDownloaderSupplier().get());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DelegatingDownloader.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DelegatingDownloader.java
new file mode 100644
index 0000000..6000262
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DelegatingDownloader.java
@@ -0,0 +1,64 @@
+// Copyright 2020 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.bazel.repository.downloader;
+
+import com.google.common.base.Optional;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * A {@link Downloader} that delegates to another Downloader. Primarily useful for mutable
+ * dependency injection.
+ */
+public class DelegatingDownloader implements Downloader {
+  private final Downloader defaultDelegate;
+  @Nullable private Downloader delegate;
+
+  public DelegatingDownloader(Downloader defaultDelegate) {
+    this.defaultDelegate = defaultDelegate;
+  }
+
+  /**
+   * Sets the {@link Downloader} to delegate to. If setDelegate(null) is called, the default
+   * delegate passed to the constructor will be used.
+   */
+  public void setDelegate(@Nullable Downloader delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void download(
+      List<URL> urls,
+      Map<URI, Map<String, String>> authHeaders,
+      Optional<Checksum> checksum,
+      String canonicalId,
+      Path destination,
+      ExtendedEventHandler eventHandler,
+      Map<String, String> clientEnv)
+      throws IOException, InterruptedException {
+    Downloader downloader = defaultDelegate;
+    if (delegate != null) {
+      downloader = delegate;
+    }
+    downloader.download(
+        urls, authHeaders, checksum, canonicalId, destination, eventHandler, clientEnv);
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/BUILD b/src/main/java/com/google/devtools/build/lib/remote/BUILD
index 69bc4de..0410d33 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/BUILD
@@ -51,12 +51,14 @@
         "//src/main/java/com/google/devtools/build/lib/actions",
         "//src/main/java/com/google/devtools/build/lib/analysis/platform:platform_utils",
         "//src/main/java/com/google/devtools/build/lib/authandtls",
+        "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/buildeventstream",
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/concurrent",
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/remote/common",
         "//src/main/java/com/google/devtools/build/lib/remote/disk",
+        "//src/main/java/com/google/devtools/build/lib/remote/downloader",
         "//src/main/java/com/google/devtools/build/lib/remote/http",
         "//src/main/java/com/google/devtools/build/lib/remote/logging",
         "//src/main/java/com/google/devtools/build/lib/remote/merkletree",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
index 7a342f1..67d7515 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
@@ -34,6 +34,7 @@
 import com.google.devtools.build.lib.analysis.test.TestProvider;
 import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
 import com.google.devtools.build.lib.authandtls.GoogleAuthUtils;
+import com.google.devtools.build.lib.bazel.repository.downloader.Downloader;
 import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
 import com.google.devtools.build.lib.buildeventstream.LocalFilesArtifactUploader;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
@@ -43,6 +44,7 @@
 import com.google.devtools.build.lib.exec.ExecutorBuilder;
 import com.google.devtools.build.lib.packages.TargetUtils;
 import com.google.devtools.build.lib.remote.common.RemoteCacheClient;
+import com.google.devtools.build.lib.remote.downloader.GrpcRemoteDownloader;
 import com.google.devtools.build.lib.remote.logging.LoggingInterceptor;
 import com.google.devtools.build.lib.remote.options.RemoteOptions;
 import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
@@ -57,6 +59,7 @@
 import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutorFactory;
 import com.google.devtools.build.lib.runtime.ServerBuilder;
 import com.google.devtools.build.lib.skyframe.AspectValue;
+import com.google.devtools.build.lib.skyframe.MutableSupplier;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
@@ -70,6 +73,7 @@
 import io.grpc.Context;
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Executors;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -95,11 +99,14 @@
   private final RepositoryRemoteExecutorFactoryDelegate repositoryRemoteExecutorFactoryDelegate =
       new RepositoryRemoteExecutorFactoryDelegate();
 
+  private final MutableSupplier<Downloader> remoteDownloaderSupplier = new MutableSupplier<>();
+
   @Override
   public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builder) {
     builder.addBuildEventArtifactUploaderFactory(
         buildEventArtifactUploaderFactoryDelegate, "remote");
     builder.setRepositoryRemoteExecutorFactory(repositoryRemoteExecutorFactoryDelegate);
+    builder.setDownloaderSupplier(remoteDownloaderSupplier);
   }
 
   /** Returns whether remote execution should be available. */
@@ -107,6 +114,11 @@
     return !Strings.isNullOrEmpty(options.remoteExecutor);
   }
 
+  /** Returns whether remote downloading should be available. */
+  private static boolean shouldEnableRemoteDownloader(RemoteOptions options) {
+    return !Strings.isNullOrEmpty(options.remoteDownloader);
+  }
+
   private void verifyServerCapabilities(
       RemoteOptions remoteOptions,
       ReferenceCountedChannel channel,
@@ -160,6 +172,13 @@
     boolean enableHttpCache = RemoteCacheClientFactory.isHttpCache(remoteOptions);
     boolean enableGrpcCache = GrpcCacheClient.isRemoteCacheOptions(remoteOptions);
     boolean enableRemoteExecution = shouldEnableRemoteExecution(remoteOptions);
+    boolean enableRemoteDownloader = shouldEnableRemoteDownloader(remoteOptions);
+
+    if (enableRemoteDownloader && !enableGrpcCache) {
+      throw new AbruptExitException(
+          "The remote downloader can only be used in combination with gRPC caching",
+          ExitCode.COMMAND_LINE_ERROR);
+    }
 
     if (!enableDiskCache && !enableHttpCache && !enableGrpcCache && !enableRemoteExecution) {
       // Quit if no remote caching or execution was enabled.
@@ -208,6 +227,7 @@
 
       ReferenceCountedChannel execChannel = null;
       ReferenceCountedChannel cacheChannel = null;
+      ReferenceCountedChannel downloaderChannel = null;
       if (enableRemoteExecution) {
         ImmutableList.Builder<ClientInterceptor> interceptors = ImmutableList.builder();
         interceptors.add(TracingMetadataUtils.newExecHeadersInterceptor(remoteOptions));
@@ -242,6 +262,25 @@
                 interceptors.build());
       }
 
+      if (enableRemoteDownloader) {
+        // Create a separate channel if --remote_downloader and --remote_cache point to different
+        // endpoints.
+        if (remoteOptions.remoteDownloader.equals(remoteOptions.remoteCache)) {
+          downloaderChannel = cacheChannel.retain();
+        } else {
+          ImmutableList.Builder<ClientInterceptor> interceptors = ImmutableList.builder();
+          if (loggingInterceptor != null) {
+            interceptors.add(loggingInterceptor);
+          }
+          downloaderChannel =
+              RemoteCacheClientFactory.createGrpcChannel(
+                  remoteOptions.remoteDownloader,
+                  remoteOptions.remoteProxy,
+                  authAndTlsOptions,
+                  interceptors.build());
+        }
+      }
+
       CallCredentials credentials = GoogleAuthUtils.newCallCredentials(authAndTlsOptions);
       RemoteRetrier retrier =
           new RemoteRetrier(
@@ -289,6 +328,9 @@
               requestContext,
               remoteOptions.remoteInstanceName));
 
+      Context repoContext =
+          TracingMetadataUtils.contextWithMetadata(buildRequestId, invocationId, "repository_rule");
+
       if (enableRemoteExecution) {
         RemoteRetrier execRetrier =
             new RemoteRetrier(
@@ -308,9 +350,6 @@
         actionContextProvider =
             RemoteActionContextProvider.createForRemoteExecution(
                 env, remoteCache, remoteExecutor, retryScheduler, digestUtil, logDir);
-        Context repoContext =
-            TracingMetadataUtils.contextWithMetadata(
-                buildRequestId, invocationId, "repository_rule");
         repositoryRemoteExecutorFactoryDelegate.init(
             new RemoteRepositoryRemoteExecutorFactory(
                 remoteCache,
@@ -336,6 +375,18 @@
             RemoteActionContextProvider.createForRemoteCaching(
                 env, remoteCache, retryScheduler, digestUtil);
       }
+
+      if (enableRemoteDownloader) {
+        remoteDownloaderSupplier.set(
+            new GrpcRemoteDownloader(
+                downloaderChannel.retain(),
+                Optional.ofNullable(credentials),
+                retrier,
+                repoContext,
+                cacheClient,
+                remoteOptions));
+        downloaderChannel.release();
+      }
     } catch (IOException e) {
       env.getReporter().handle(Event.error(e.getMessage()));
       env.getBlazeModuleEnvironment()
@@ -468,6 +519,7 @@
 
     buildEventArtifactUploaderFactoryDelegate.reset();
     repositoryRemoteExecutorFactoryDelegate.reset();
+    remoteDownloaderSupplier.set(null);
     actionContextProvider = null;
     actionInputFetcher = null;
     remoteOutputsMode = null;
diff --git a/src/main/java/com/google/devtools/build/lib/remote/downloader/BUILD b/src/main/java/com/google/devtools/build/lib/remote/downloader/BUILD
index 1035c7d..862215f 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/downloader/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/downloader/BUILD
@@ -15,7 +15,6 @@
     deps = [
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
-        "//src/main/java/com/google/devtools/build/lib/remote",
         "//src/main/java/com/google/devtools/build/lib/remote:ReferenceCountedChannel",
         "//src/main/java/com/google/devtools/build/lib/remote:Retrier",
         "//src/main/java/com/google/devtools/build/lib/remote/common",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
index 4e4e27e..9c9f99f 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
@@ -85,7 +85,16 @@
               + " https://docs.bazel.build/versions/master/remote-caching.html")
   public String remoteCache;
 
-  public final String remoteDownloader = "";
+  @Option(
+      name = "experimental_remote_downloader",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.REMOTE,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help =
+          "A URI of a remote downloader endpoint. The supported schemas are grpc and grpcs"
+              + " (grpc with TLS enabled). If no schema is provided bazel will default to grpcs."
+              + " Specify grpc:// schema to disable TLS.")
+  public String remoteDownloader;
 
   @Option(
       name = "remote_header",
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
index 1d1d640..1592416 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -34,6 +34,7 @@
 import com.google.devtools.build.lib.analysis.ServerDirectories;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory;
+import com.google.devtools.build.lib.bazel.repository.downloader.Downloader;
 import com.google.devtools.build.lib.bugreport.BugReport;
 import com.google.devtools.build.lib.bugreport.BugReporter;
 import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
@@ -123,6 +124,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.logging.Handler;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
@@ -175,6 +177,7 @@
   private final ImmutableMap<String, AuthHeadersProvider> authHeadersProviderMap;
   private final RetainedHeapLimiter retainedHeapLimiter = new RetainedHeapLimiter();
   @Nullable private final RepositoryRemoteExecutorFactory repositoryRemoteExecutorFactory;
+  private final Supplier<Downloader> downloaderSupplier;
 
   // Workspace state (currently exactly one workspace per server)
   private BlazeWorkspace workspace;
@@ -201,7 +204,8 @@
       String productName,
       BuildEventArtifactUploaderFactoryMap buildEventArtifactUploaderFactoryMap,
       ImmutableMap<String, AuthHeadersProvider> authHeadersProviderMap,
-      RepositoryRemoteExecutorFactory repositoryRemoteExecutorFactory) {
+      RepositoryRemoteExecutorFactory repositoryRemoteExecutorFactory,
+      Supplier<Downloader> downloaderSupplier) {
     // Server state
     this.fileSystem = fileSystem;
     this.blazeModules = blazeModules;
@@ -231,6 +235,7 @@
     this.authHeadersProviderMap =
         Preconditions.checkNotNull(authHeadersProviderMap, "authHeadersProviderMap");
     this.repositoryRemoteExecutorFactory = repositoryRemoteExecutorFactory;
+    this.downloaderSupplier = downloaderSupplier;
   }
 
   public BlazeWorkspace initWorkspace(BlazeDirectories directories, BinTools binTools)
@@ -1448,6 +1453,10 @@
     return repositoryRemoteExecutorFactory;
   }
 
+  public Supplier<Downloader> getDownloaderSupplier() {
+    return downloaderSupplier;
+  }
+
   /**
    * A builder for {@link BlazeRuntime} objects. The only required fields are the {@link
    * BlazeDirectories}, and the {@link RuleClassProvider} (except for testing). All other fields
@@ -1589,7 +1598,8 @@
           productName,
           serverBuilder.getBuildEventArtifactUploaderMap(),
           serverBuilder.getAuthHeadersProvidersMap(),
-          serverBuilder.getRepositoryRemoteExecutorFactory());
+          serverBuilder.getRepositoryRemoteExecutorFactory(),
+          serverBuilder.getDownloaderSupplier());
     }
 
     public Builder setProductName(String productName) {
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java b/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java
index b173ee5..064bdc6 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/ServerBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.bazel.repository.downloader.Downloader;
 import com.google.devtools.build.lib.packages.PackageFactory;
 import com.google.devtools.build.lib.query2.QueryEnvironmentFactory;
 import com.google.devtools.build.lib.query2.common.AbstractBlazeQueryEnvironment;
@@ -24,6 +25,7 @@
 import com.google.devtools.build.lib.query2.query.output.OutputFormatter;
 import com.google.devtools.build.lib.runtime.commands.InfoItem;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
+import java.util.function.Supplier;
 
 /**
  * Builder class to create a {@link BlazeRuntime} instance. This class is part of the module API,
@@ -44,6 +46,7 @@
   private final ImmutableMap.Builder<String, AuthHeadersProvider> authHeadersProvidersMap =
       ImmutableMap.builder();
   private RepositoryRemoteExecutorFactory repositoryRemoteExecutorFactory;
+  private Supplier<Downloader> downloaderSupplier = () -> null;
 
   @VisibleForTesting
   public ServerBuilder() {}
@@ -88,6 +91,10 @@
     return repositoryRemoteExecutorFactory;
   }
 
+  public Supplier<Downloader> getDownloaderSupplier() {
+    return downloaderSupplier;
+  }
+
   /**
    * Merges the given invocation policy into the per-server invocation policy. While this can accept
    * any number of policies, the end result is order-dependent if multiple policies attempt to
@@ -172,6 +179,11 @@
     return this;
   }
 
+  public ServerBuilder setDownloaderSupplier(Supplier<Downloader> downloaderSupplier) {
+    this.downloaderSupplier = downloaderSupplier;
+    return this;
+  }
+
   /**
    * Register a provider of authentication headers that blaze modules can use. See {@link
    * AuthHeadersProvider} for more details.