blob: 9886abc6ec2f5af8fd520f7a1a3e955016ab5c2f [file] [log] [blame]
// Copyright 2016 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.remote;
import static java.util.concurrent.TimeUnit.SECONDS;
import build.bazel.remote.execution.v2.DigestFunction;
import com.github.benmanes.caffeine.cache.Cache;
import com.google.auth.Credentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget;
import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
import com.google.devtools.build.lib.authandtls.CallCredentialsProvider;
import com.google.devtools.build.lib.authandtls.GoogleAuthUtils;
import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperEnvironment;
import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule;
import com.google.devtools.build.lib.authandtls.credentialhelper.GetCredentialsResponse;
import com.google.devtools.build.lib.bazel.repository.downloader.Downloader;
import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
import com.google.devtools.build.lib.buildeventstream.LocalFilesArtifactUploader;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.clock.JavaClock;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.exec.ExecutorBuilder;
import com.google.devtools.build.lib.exec.ModuleActionContextRegistry;
import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.remote.LeaseService.LeaseExtension;
import com.google.devtools.build.lib.remote.RemoteServerCapabilities.ServerCapabilitiesRequirement;
import com.google.devtools.build.lib.remote.circuitbreaker.CircuitBreakerFactory;
import com.google.devtools.build.lib.remote.common.RemoteCacheClient;
import com.google.devtools.build.lib.remote.common.RemoteExecutionClient;
import com.google.devtools.build.lib.remote.downloader.GrpcRemoteDownloader;
import com.google.devtools.build.lib.remote.http.HttpException;
import com.google.devtools.build.lib.remote.logging.LoggingInterceptor;
import com.google.devtools.build.lib.remote.logging.RemoteExecutionLog.LogEntry;
import com.google.devtools.build.lib.remote.options.RemoteOptions;
import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
import com.google.devtools.build.lib.remote.util.DigestUtil;
import com.google.devtools.build.lib.remote.util.TempPathGenerator;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.remote.util.Utils;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.BlockWaitingModule;
import com.google.devtools.build.lib.runtime.BuildEventArtifactUploaderFactory;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.CommandLinePathFactory;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutor;
import com.google.devtools.build.lib.runtime.RepositoryRemoteExecutorFactory;
import com.google.devtools.build.lib.runtime.ServerBuilder;
import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Execution;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution;
import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution.Code;
import com.google.devtools.build.lib.skyframe.MutableSupplier;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.AsynchronousMessageOutputStream;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.OutputPermissions;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingResult;
import com.google.devtools.common.options.RegexPatternOption;
import io.grpc.CallCredentials;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ClosedChannelException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** RemoteModule provides distributed cache and remote execution for Bazel. */
public final class RemoteModule extends BlazeModule {
private final ListeningScheduledExecutorService retryScheduler =
MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1));
@Nullable private AsynchronousMessageOutputStream<LogEntry> rpcLogFile;
@Nullable private ExecutorService executorService;
@Nullable private RemoteActionContextProvider actionContextProvider;
@Nullable private RemoteActionInputFetcher actionInputFetcher;
@Nullable private RemoteOptions remoteOptions;
@Nullable private CommandEnvironment env;
@Nullable private OutputService outputService;
@Nullable private TempPathGenerator tempPathGenerator;
@Nullable private BlockWaitingModule blockWaitingModule;
@Nullable private RemoteOutputChecker remoteOutputChecker;
@Nullable private String lastBuildId;
private ChannelFactory channelFactory =
new ChannelFactory() {
@Override
public ManagedChannel newChannel(
String target,
String proxy,
AuthAndTLSOptions options,
List<ClientInterceptor> interceptors)
throws IOException {
return GoogleAuthUtils.newChannel(
executorService,
target,
proxy,
options,
interceptors.isEmpty() ? null : interceptors);
}
};
private final BuildEventArtifactUploaderFactoryDelegate
buildEventArtifactUploaderFactoryDelegate = new BuildEventArtifactUploaderFactoryDelegate();
private final RepositoryRemoteExecutorFactoryDelegate repositoryRemoteExecutorFactoryDelegate =
new RepositoryRemoteExecutorFactoryDelegate();
private final MutableSupplier<Downloader> remoteDownloaderSupplier = new MutableSupplier<>();
private CredentialModule credentialModule;
@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. */
public static boolean shouldEnableRemoteExecution(RemoteOptions options) {
return !Strings.isNullOrEmpty(options.remoteExecutor);
}
/** Returns whether remote downloading should be available. */
private static boolean shouldEnableRemoteDownloader(RemoteOptions options) {
return !Strings.isNullOrEmpty(options.remoteDownloader);
}
public static final Predicate<? super Exception> RETRIABLE_HTTP_ERRORS =
e -> {
boolean retry = false;
if (e instanceof ClosedChannelException) {
retry = true;
} else if (e instanceof HttpException httpException) {
int status = httpException.response().status().code();
retry =
status == HttpResponseStatus.INTERNAL_SERVER_ERROR.code()
|| status == HttpResponseStatus.BAD_GATEWAY.code()
|| status == HttpResponseStatus.SERVICE_UNAVAILABLE.code()
|| status == HttpResponseStatus.GATEWAY_TIMEOUT.code();
} else if (e instanceof IOException) {
String msg = Ascii.toLowerCase(e.getMessage());
if (msg.contains("connection reset by peer")) {
retry = true;
} else if (msg.contains("operation timed out")) {
retry = true;
}
} else {
// Workaround for a netty bug: https://github.com/netty/netty/issues/11815. Remove this
// once it is fixed in the upstream.
if (e instanceof DecoderException
&& e.getMessage().endsWith("functions:OPENSSL_internal:BAD_DECRYPT")) {
retry = true;
}
}
return retry;
};
private void initHttpAndDiskCache(
CommandEnvironment env,
Credentials credentials,
AuthAndTLSOptions authAndTlsOptions,
RemoteOptions remoteOptions,
DigestUtil digestUtil,
ExecutorService executorService) {
RemoteCacheClient cacheClient;
Retrier.CircuitBreaker circuitBreaker =
CircuitBreakerFactory.createCircuitBreaker(remoteOptions);
try {
cacheClient =
RemoteCacheClientFactory.create(
remoteOptions,
credentials,
authAndTlsOptions,
Preconditions.checkNotNull(env.getWorkingDirectory(), "workingDirectory"),
digestUtil,
executorService,
new RemoteRetrier(
remoteOptions, RETRIABLE_HTTP_ERRORS, retryScheduler, circuitBreaker));
} catch (IOException e) {
handleInitFailure(env, e, Code.CACHE_INIT_FAILURE);
return;
}
RemoteCache remoteCache = new RemoteCache(cacheClient, remoteOptions, digestUtil);
actionContextProvider =
RemoteActionContextProvider.createForRemoteCaching(
executorService,
env,
remoteCache,
/* retryScheduler= */ null,
digestUtil,
remoteOutputChecker,
outputService);
}
@Override
public void workspaceInit(
BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
Preconditions.checkState(blockWaitingModule == null, "blockWaitingModule must be null");
Preconditions.checkState(credentialModule == null, "credentialModule must be null");
blockWaitingModule =
Preconditions.checkNotNull(runtime.getBlazeModule(BlockWaitingModule.class));
credentialModule = Preconditions.checkNotNull(runtime.getBlazeModule(CredentialModule.class));
}
@Override
public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
Preconditions.checkState(actionContextProvider == null, "actionContextProvider must be null");
Preconditions.checkState(actionInputFetcher == null, "actionInputFetcher must be null");
Preconditions.checkState(remoteOptions == null, "remoteOptions must be null");
Preconditions.checkState(this.env == null, "env must be null");
Preconditions.checkState(tempPathGenerator == null, "tempPathGenerator must be null");
Preconditions.checkState(remoteOutputChecker == null, "remoteOutputChecker must be null");
Preconditions.checkState(outputService == null, "remoteOutputService must be null");
RemoteOptions remoteOptions = env.getOptions().getOptions(RemoteOptions.class);
if (remoteOptions == null) {
// Quit if no supported command is being used. See getCommandOptions for details.
return;
}
this.remoteOptions = remoteOptions;
this.env = env;
AuthAndTLSOptions authAndTlsOptions = env.getOptions().getOptions(AuthAndTLSOptions.class);
DigestHashFunction hashFn = env.getRuntime().getFileSystem().getDigestFunction();
DigestUtil digestUtil = new DigestUtil(env.getXattrProvider(), hashFn);
boolean verboseFailures = false;
ExecutionOptions executionOptions = env.getOptions().getOptions(ExecutionOptions.class);
if (executionOptions != null) {
verboseFailures = executionOptions.verboseFailures;
}
boolean enableDiskCache = RemoteCacheClientFactory.isDiskCache(remoteOptions);
boolean enableHttpCache = RemoteCacheClientFactory.isHttpCache(remoteOptions);
boolean enableRemoteExecution = shouldEnableRemoteExecution(remoteOptions);
// If --remote_cache is empty but --remote_executor is not, endpoint for cache should be the one
// for execution.
if (enableRemoteExecution && Strings.isNullOrEmpty(remoteOptions.remoteCache)) {
remoteOptions.remoteCache = remoteOptions.remoteExecutor;
}
boolean enableGrpcCache = GrpcCacheClient.isRemoteCacheOptions(remoteOptions);
boolean enableRemoteDownloader = shouldEnableRemoteDownloader(remoteOptions);
boolean enableRemoteOutputService = !Strings.isNullOrEmpty(remoteOptions.remoteOutputService);
if (enableRemoteDownloader && !enableGrpcCache) {
throw createOptionsExitException(
"The remote downloader can only be used in combination with gRPC caching",
FailureDetails.RemoteOptions.Code.DOWNLOADER_WITHOUT_GRPC_CACHE);
}
if (enableRemoteOutputService) {
if (enableDiskCache) {
env.getReporter()
.handle(
Event.warn(
"--disk_cache is ignored when --experimental_remote_output_service is set."));
}
if (Strings.isNullOrEmpty(remoteOptions.remoteCache)) {
throw createOptionsExitException(
"--experimental_remote_output_service can only be used in combination with"
+ " --remote_cache or --remote_executor.",
FailureDetails.RemoteOptions.Code.EXECUTION_WITH_INVALID_CACHE);
}
}
if (!enableDiskCache && !enableHttpCache && !enableGrpcCache && !enableRemoteExecution) {
// Quit if no remote caching or execution was enabled.
actionContextProvider =
RemoteActionContextProvider.createForPlaceholder(env, retryScheduler, digestUtil);
return;
}
if (enableHttpCache && enableRemoteExecution) {
throw createOptionsExitException(
"Cannot combine gRPC based remote execution with HTTP-based caching",
FailureDetails.RemoteOptions.Code.EXECUTION_WITH_INVALID_CACHE);
}
boolean enableScrubbing = remoteOptions.scrubber != null;
if (enableScrubbing && enableRemoteExecution) {
env.getReporter()
.handle(
Event.warn(
"Cache key scrubbing is incompatible with remote execution. Actions that are"
+ " scrubbed per the --experimental_remote_scrubbing_config configuration"
+ " file will be executed locally instead."));
}
if (digestUtil.getDigestFunction() == DigestFunction.Value.UNKNOWN) {
throw new AbruptExitException(
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(String.format("Unsupported digest function: %s", hashFn))
.setExecution(Execution.newBuilder().setCode(Execution.Code.EXECUTION_UNKNOWN))
.build()));
}
// TODO(bazel-team): Consider adding a warning or more validation if the remoteDownloadRegex is
// used without Build without the Bytes.
ImmutableList.Builder<Pattern> patternsToDownloadBuilder = ImmutableList.builder();
if (remoteOptions.remoteOutputsMode != RemoteOutputsMode.ALL) {
for (RegexPatternOption patternOption : remoteOptions.remoteDownloadRegex) {
patternsToDownloadBuilder.add(patternOption.regexPattern());
}
}
remoteOutputChecker =
new RemoteOutputChecker(
new JavaClock(),
env.getCommandName(),
remoteOptions.remoteOutputsMode,
patternsToDownloadBuilder.build());
env.getEventBus().register(this);
String invocationId = env.getCommandId().toString();
String buildRequestId = env.getBuildRequestId();
env.getReporter().handle(Event.info(String.format("Invocation ID: %s", invocationId)));
RxJavaPlugins.setErrorHandler(
error -> env.getReporter().handle(Event.error(Throwables.getStackTraceAsString(error))));
Path logDir =
env.getOutputBase().getRelative(env.getRuntime().getProductName() + "-remote-logs");
cleanAndCreateRemoteLogsDir(logDir);
BuildRequestOptions buildRequestOptions =
env.getOptions().getOptions(BuildRequestOptions.class);
int jobs = 0;
if (buildRequestOptions != null) {
jobs = buildRequestOptions.jobs;
}
ThreadFactory threadFactory =
new ThreadFactoryBuilder().setNameFormat("remote-executor-%d").build();
if (jobs != 0) {
ThreadPoolExecutor tpe =
new ThreadPoolExecutor(
jobs, jobs, 60L, SECONDS, new LinkedBlockingQueue<>(), threadFactory);
tpe.allowCoreThreadTimeOut(true);
executorService = tpe;
} else {
executorService = Executors.newCachedThreadPool(threadFactory);
}
Credentials credentials;
try {
credentials =
createCredentials(
CredentialHelperEnvironment.newBuilder()
.setEventReporter(env.getReporter())
.setWorkspacePath(env.getWorkspace())
.setClientEnvironment(env.getClientEnv())
.setHelperExecutionTimeout(authAndTlsOptions.credentialHelperTimeout)
.build(),
credentialModule.getCredentialCache(),
env.getCommandLinePathFactory(),
env.getRuntime().getFileSystem(),
authAndTlsOptions,
remoteOptions);
} catch (IOException e) {
handleInitFailure(env, e, Code.CREDENTIALS_INIT_FAILURE);
return;
}
// The number of concurrent requests for one connection to a gRPC server is limited by
// MAX_CONCURRENT_STREAMS which is normally being 100+. We assume 50 concurrent requests for
// each connection should be fairly well. The number of connections opened by one channel is
// based on the resolved IPs of that server. We assume servers normally have 2 IPs. So the
// max concurrency per connection is 100.
int maxConcurrencyPerConnection = 100;
int maxConnections = 0;
if (remoteOptions.remoteMaxConnections > 0) {
maxConnections = remoteOptions.remoteMaxConnections;
}
Retrier.CircuitBreaker circuitBreaker =
CircuitBreakerFactory.createCircuitBreaker(remoteOptions);
RemoteRetrier retrier =
new RemoteRetrier(
remoteOptions, RemoteRetrier.RETRIABLE_GRPC_ERRORS, retryScheduler, circuitBreaker);
if (!Strings.isNullOrEmpty(remoteOptions.remoteOutputService)) {
var bazelOutputServiceChannel =
createChannel(
executorService,
remoteOptions,
// Don't use auth flags for remote output service
Options.getDefaults(AuthAndTLSOptions.class),
null,
null,
channelFactory,
remoteOptions.remoteOutputService,
null,
maxConcurrencyPerConnection,
maxConnections,
verboseFailures,
env.getReporter(),
null,
digestUtil.getDigestFunction(),
ServerCapabilitiesRequirement.NONE);
outputService =
new BazelOutputService(
env.getOutputBase(),
env::getExecRoot,
() -> env.getDirectories().getOutputPath(env.getWorkspaceName()),
digestUtil.getDigestFunction(),
remoteOptions.remoteCache,
remoteOptions.remoteInstanceName,
remoteOptions.remoteOutputServiceOutputPathPrefix,
verboseFailures,
retrier,
bazelOutputServiceChannel,
lastBuildId);
} else {
outputService = new RemoteOutputService(env);
}
if ((enableHttpCache || enableDiskCache) && !enableGrpcCache) {
initHttpAndDiskCache(
env, credentials, authAndTlsOptions, remoteOptions, digestUtil, executorService);
return;
}
ClientInterceptor loggingInterceptor = null;
if (remoteOptions.remoteGrpcLog != null) {
try {
rpcLogFile =
new AsynchronousMessageOutputStream<>(
env.getWorkingDirectory().getRelative(remoteOptions.remoteGrpcLog));
} catch (IOException e) {
handleInitFailure(env, e, Code.RPC_LOG_FAILURE);
return;
}
loggingInterceptor = new LoggingInterceptor(rpcLogFile, env.getRuntime().getClock());
}
CallCredentialsProvider callCredentialsProvider =
GoogleAuthUtils.newCallCredentialsProvider(credentials);
CallCredentials callCredentials = callCredentialsProvider.getCallCredentials();
RemoteServerCapabilities rsc =
new RemoteServerCapabilities(
buildRequestId,
invocationId,
remoteOptions.remoteInstanceName,
callCredentials,
remoteOptions.remoteTimeout.getSeconds(),
retrier);
ReferenceCountedChannel execChannel = null;
ReferenceCountedChannel cacheChannel = null;
// We only check required capabilities for a given endpoint.
//
// If --remote_executor and --remote_cache point to the same endpoint, we require that
// endpoint has both execution and cache capabilities.
//
// If they point to different endpoints, we check the endpoint with execution or cache
// capabilities respectively.
try (var s = Profiler.instance().profile("init channel and check server capabilities")) {
if (enableRemoteExecution) {
// Create a separate channel if --remote_executor and --remote_cache point to different
// endpoints.
if (remoteOptions.remoteCache.equals(remoteOptions.remoteExecutor)) {
execChannel =
createChannel(
executorService,
remoteOptions,
authAndTlsOptions,
TracingMetadataUtils.newExecHeadersInterceptor(remoteOptions),
loggingInterceptor,
channelFactory,
remoteOptions.remoteExecutor,
remoteOptions.remoteProxy,
maxConcurrencyPerConnection,
maxConnections,
verboseFailures,
env.getReporter(),
rsc,
digestUtil.getDigestFunction(),
ServerCapabilitiesRequirement.EXECUTION_AND_CACHE);
cacheChannel = execChannel.retain();
} else {
execChannel =
createChannel(
executorService,
remoteOptions,
authAndTlsOptions,
TracingMetadataUtils.newExecHeadersInterceptor(remoteOptions),
loggingInterceptor,
channelFactory,
remoteOptions.remoteExecutor,
remoteOptions.remoteProxy,
maxConcurrencyPerConnection,
maxConnections,
verboseFailures,
env.getReporter(),
rsc,
digestUtil.getDigestFunction(),
ServerCapabilitiesRequirement.EXECUTION);
}
}
if (cacheChannel == null) {
cacheChannel =
createChannel(
executorService,
remoteOptions,
authAndTlsOptions,
TracingMetadataUtils.newCacheHeadersInterceptor(remoteOptions),
loggingInterceptor,
channelFactory,
remoteOptions.remoteCache,
remoteOptions.remoteProxy,
maxConcurrencyPerConnection,
maxConnections,
verboseFailures,
env.getReporter(),
rsc,
digestUtil.getDigestFunction(),
ServerCapabilitiesRequirement.CACHE);
}
}
RemoteCacheClient cacheClient =
new GrpcCacheClient(
cacheChannel.retain(), callCredentialsProvider, remoteOptions, retrier, digestUtil);
cacheChannel.release();
if (enableRemoteExecution) {
if (enableDiskCache) {
try {
cacheClient =
RemoteCacheClientFactory.createDiskAndRemoteClient(
env.getWorkingDirectory(),
remoteOptions.diskCache,
digestUtil,
executorService,
remoteOptions.remoteVerifyDownloads,
cacheClient);
} catch (Exception e) {
handleInitFailure(env, e, Code.CACHE_INIT_FAILURE);
return;
}
}
RemoteExecutionClient remoteExecutor;
if (remoteOptions.remoteExecutionKeepalive) {
RemoteRetrier execRetrier =
new RemoteRetrier(
remoteOptions,
RemoteRetrier.RETRIABLE_GRPC_ERRORS, // Handle NOT_FOUND internally
retryScheduler,
circuitBreaker);
remoteExecutor =
new ExperimentalGrpcRemoteExecutor(
remoteOptions, execChannel.retain(), callCredentialsProvider, execRetrier);
} else {
RemoteRetrier execRetrier =
new RemoteRetrier(
remoteOptions,
RemoteRetrier.RETRIABLE_GRPC_EXEC_ERRORS,
retryScheduler,
circuitBreaker);
remoteExecutor =
new GrpcRemoteExecutor(execChannel.retain(), callCredentialsProvider, execRetrier);
}
execChannel.release();
RemoteExecutionCache remoteCache =
new RemoteExecutionCache(cacheClient, remoteOptions, digestUtil);
actionContextProvider =
RemoteActionContextProvider.createForRemoteExecution(
executorService,
env,
remoteCache,
remoteExecutor,
retryScheduler,
digestUtil,
logDir,
remoteOutputChecker,
outputService);
repositoryRemoteExecutorFactoryDelegate.init(
new RemoteRepositoryRemoteExecutorFactory(
remoteCache,
remoteExecutor,
digestUtil,
buildRequestId,
invocationId,
remoteOptions.remoteInstanceName,
remoteOptions.remoteAcceptCached,
env.getReporter()));
} else {
if (enableDiskCache) {
try {
cacheClient =
RemoteCacheClientFactory.createDiskAndRemoteClient(
env.getWorkingDirectory(),
remoteOptions.diskCache,
digestUtil,
executorService,
remoteOptions.remoteVerifyDownloads,
cacheClient);
} catch (Exception e) {
handleInitFailure(env, e, Code.CACHE_INIT_FAILURE);
return;
}
}
RemoteCache remoteCache = new RemoteCache(cacheClient, remoteOptions, digestUtil);
actionContextProvider =
RemoteActionContextProvider.createForRemoteCaching(
executorService,
env,
remoteCache,
retryScheduler,
digestUtil,
remoteOutputChecker,
outputService);
}
buildEventArtifactUploaderFactoryDelegate.init(
new ByteStreamBuildEventArtifactUploaderFactory(
executorService,
env.getReporter(),
verboseFailures,
actionContextProvider.getRemoteCache(),
remoteOptions.remoteInstanceName,
remoteOptions.remoteBytestreamUriPrefix,
buildRequestId,
invocationId,
remoteOptions.remoteBuildEventUploadMode));
if (enableRemoteDownloader) {
ReferenceCountedChannel downloaderChannel;
// Create a separate channel if --remote_downloader and --remote_cache point to different
// endpoints.
if (remoteOptions.remoteDownloader.equals(remoteOptions.remoteCache)) {
downloaderChannel = cacheChannel.retain();
} else {
downloaderChannel =
createChannel(
executorService,
remoteOptions,
authAndTlsOptions,
/* headersInterceptor= */ null,
loggingInterceptor,
channelFactory,
remoteOptions.remoteDownloader,
remoteOptions.remoteProxy,
maxConcurrencyPerConnection,
maxConnections,
verboseFailures,
env.getReporter(),
rsc,
digestUtil.getDigestFunction(),
ServerCapabilitiesRequirement.NONE);
}
Downloader fallbackDownloader = null;
if (remoteOptions.remoteDownloaderLocalFallback) {
fallbackDownloader = new HttpDownloader();
}
remoteDownloaderSupplier.set(
new GrpcRemoteDownloader(
buildRequestId,
invocationId,
downloaderChannel.retain(),
Optional.ofNullable(callCredentials),
retrier,
cacheClient,
digestUtil.getDigestFunction(),
remoteOptions,
verboseFailures,
fallbackDownloader));
downloaderChannel.release();
}
}
private static ReferenceCountedChannel createChannel(
ExecutorService executorService,
RemoteOptions remoteOptions,
AuthAndTLSOptions authAndTlsOptions,
@Nullable ClientInterceptor headersInterceptor,
@Nullable ClientInterceptor loggingInterceptor,
ChannelFactory channelFactory,
String target,
String proxy,
int maxConcurrencyPerConnection,
int maxConnections,
boolean verboseFailures,
Reporter reporter,
@Nullable RemoteServerCapabilities remoteServerCapabilities,
DigestFunction.Value digestFunction,
ServerCapabilitiesRequirement requirement) {
ImmutableList.Builder<ClientInterceptor> interceptors = ImmutableList.builder();
if (headersInterceptor != null) {
interceptors.add(headersInterceptor);
}
if (loggingInterceptor != null) {
interceptors.add(loggingInterceptor);
}
var channel =
new ReferenceCountedChannel(
new GoogleChannelConnectionFactory(
channelFactory,
target,
proxy,
remoteOptions,
authAndTlsOptions,
interceptors.build(),
maxConcurrencyPerConnection,
verboseFailures,
reporter,
remoteServerCapabilities,
digestFunction,
requirement),
maxConnections);
// Eagerly start creating the channel and verifying the capabilities in the background.
// TODO(tjgq): Make sure this task doesn't linger beyond afterCommand().
var unused =
executorService.submit(
() -> {
var unused2 = channel.withChannelFuture(c -> null);
});
return channel;
}
private static void handleInitFailure(
CommandEnvironment env, Exception e, Code remoteExecutionCode) {
env.getReporter().handle(Event.error(e.getMessage()));
env.getBlazeModuleEnvironment()
.exit(
createExitException(
"Error initializing RemoteModule",
ExitCode.COMMAND_LINE_ERROR,
remoteExecutionCode));
}
// This is a Skymeld-only code path. At the same time, afterAnalysis is exclusive to the
// non-Skymeld code path.
@Override
public void afterTopLevelTargetAnalysis(
CommandEnvironment env,
BuildRequest request,
BuildOptions buildOptions,
ConfiguredTarget configuredTarget) {
if (remoteOutputChecker != null) {
remoteOutputChecker.afterTopLevelTargetAnalysis(
configuredTarget, request::getTopLevelArtifactContext);
}
if (shouldParseNoCacheOutputs()) {
parseNoCacheOutputsFromSingleConfiguredTarget(
Preconditions.checkNotNull(buildEventArtifactUploaderFactoryDelegate.get()),
configuredTarget);
}
}
@Override
public void afterSingleAspectAnalysis(BuildRequest request, ConfiguredAspect configuredTarget) {
if (remoteOutputChecker != null) {
remoteOutputChecker.afterAspectAnalysis(
configuredTarget, request::getTopLevelArtifactContext);
}
}
@Override
public void afterSingleTestAnalysis(BuildRequest request, ConfiguredTarget configuredTarget) {
if (remoteOutputChecker != null) {
remoteOutputChecker.afterTestAnalyzedEvent(configuredTarget);
}
}
@Override
public void coverageArtifactsKnown(ImmutableSet<Artifact> coverageArtifacts) {
if (remoteOutputChecker != null) {
remoteOutputChecker.coverageArtifactsKnown(coverageArtifacts);
}
}
@Override
public void afterAnalysis(
CommandEnvironment env,
BuildRequest request,
BuildOptions buildOptions,
AnalysisResult analysisResult) {
if (remoteOutputChecker != null) {
remoteOutputChecker.afterAnalysis(analysisResult);
}
if (shouldParseNoCacheOutputs()) {
parseNoCacheOutputs(analysisResult);
}
}
// Separating the conditions for readability.
private boolean shouldParseNoCacheOutputs() {
return false;
}
private void parseNoCacheOutputs(AnalysisResult analysisResult) {
ByteStreamBuildEventArtifactUploader uploader =
Preconditions.checkNotNull(buildEventArtifactUploaderFactoryDelegate.get());
for (ConfiguredTarget configuredTarget : analysisResult.getTargetsToBuild()) {
parseNoCacheOutputsFromSingleConfiguredTarget(uploader, configuredTarget);
}
}
private void parseNoCacheOutputsFromSingleConfiguredTarget(
ByteStreamBuildEventArtifactUploader uploader, ConfiguredTarget configuredTarget) {
// This will either dereference an alias chain, or return the final ConfiguredTarget.
ConfiguredTarget actualConfiguredTarget = configuredTarget.getActual();
if (!(actualConfiguredTarget instanceof RuleConfiguredTarget ruleConfiguredTarget)) {
return;
}
for (ActionAnalysisMetadata action : ruleConfiguredTarget.getActions()) {
boolean uploadLocalResults =
Utils.shouldUploadLocalResultsToRemoteCache(remoteOptions, action.getExecutionInfo());
if (!uploadLocalResults) {
for (Artifact output : action.getOutputs()) {
if (output.isTreeArtifact()) {
uploader.omitTree(output.getPath());
} else {
uploader.omitFile(output.getPath());
}
}
}
}
}
private static void cleanAndCreateRemoteLogsDir(Path logDir) throws AbruptExitException {
try {
// Clean out old logs files.
if (logDir.exists()) {
logDir.deleteTree();
}
logDir.createDirectory();
} catch (IOException e) {
String message = String.format("Could not create base directory for remote logs: %s", logDir);
throw createExitException(
message, ExitCode.LOCAL_ENVIRONMENTAL_ERROR, Code.LOG_DIR_CLEANUP_FAILURE);
}
}
@Override
public void afterCommand() {
Preconditions.checkNotNull(blockWaitingModule, "blockWaitingModule must not be null");
// Some cleanup tasks must wait until every other BlazeModule's afterCommand() has run, as
// otherwise we might interfere with asynchronous remote downloads that are in progress.
RemoteActionContextProvider actionContextProviderRef = actionContextProvider;
TempPathGenerator tempPathGeneratorRef = tempPathGenerator;
AsynchronousMessageOutputStream<LogEntry> rpcLogFileRef = rpcLogFile;
if (actionContextProviderRef != null || tempPathGeneratorRef != null || rpcLogFileRef != null) {
blockWaitingModule.submit(
() -> afterCommandTask(actionContextProviderRef, tempPathGeneratorRef, rpcLogFileRef));
}
lastBuildId = Preconditions.checkNotNull(env).getCommandId().toString();
buildEventArtifactUploaderFactoryDelegate.reset();
repositoryRemoteExecutorFactoryDelegate.reset();
remoteDownloaderSupplier.set(null);
actionContextProvider = null;
actionInputFetcher = null;
remoteOptions = null;
env = null;
outputService = null;
tempPathGenerator = null;
rpcLogFile = null;
remoteOutputChecker = null;
}
private static void afterCommandTask(
RemoteActionContextProvider actionContextProvider,
TempPathGenerator tempPathGenerator,
AsynchronousMessageOutputStream<LogEntry> rpcLogFile)
throws AbruptExitException {
if (actionContextProvider != null) {
actionContextProvider.afterCommand();
}
if (tempPathGenerator != null) {
Path tempDir = tempPathGenerator.getTempDir();
try {
tempDir.deleteTree();
} catch (IOException ignored) {
// Intentionally ignored.
}
}
if (rpcLogFile != null) {
try {
rpcLogFile.close();
} catch (IOException e) {
throw createExitException(
"Partially wrote RPC log file",
ExitCode.LOCAL_ENVIRONMENTAL_ERROR,
Code.RPC_LOG_FAILURE);
}
}
}
@Override
public void registerSpawnStrategies(
SpawnStrategyRegistry.Builder registryBuilder, CommandEnvironment env) {
if (actionContextProvider == null) {
return;
}
RemoteOptions remoteOptions =
Preconditions.checkNotNull(
env.getOptions().getOptions(RemoteOptions.class), "RemoteOptions");
registryBuilder.setRemoteLocalFallbackStrategyIdentifier(
remoteOptions.remoteLocalFallbackStrategy);
actionContextProvider.registerRemoteSpawnStrategy(registryBuilder);
}
@Override
public void registerActionContexts(
ModuleActionContextRegistry.Builder registryBuilder,
CommandEnvironment env,
BuildRequest buildRequest) {
if (actionContextProvider == null) {
return;
}
actionContextProvider.registerSpawnCache(registryBuilder);
}
private TempPathGenerator getTempPathGenerator(CommandEnvironment env)
throws AbruptExitException {
Path tempDir = env.getActionTempsDirectory().getChild("remote");
if (tempDir.exists()) {
env.getReporter()
.handle(Event.warn("Found stale downloads from previous build, deleting..."));
try {
tempDir.deleteTree();
} catch (IOException e) {
throw new AbruptExitException(
DetailedExitCode.of(
ExitCode.LOCAL_ENVIRONMENTAL_ERROR,
FailureDetail.newBuilder()
.setMessage(
String.format("Failed to delete stale downloads: %s", e.getMessage()))
.setRemoteExecution(
RemoteExecution.newBuilder()
.setCode(Code.DOWNLOADED_INPUTS_DELETION_FAILURE))
.build()));
}
}
return new TempPathGenerator(tempDir);
}
@Override
public void executorInit(CommandEnvironment env, BuildRequest request, ExecutorBuilder builder)
throws AbruptExitException {
Preconditions.checkState(actionInputFetcher == null, "actionInputFetcher must be null");
Preconditions.checkState(tempPathGenerator == null, "tempPathGenerator must be null");
Preconditions.checkNotNull(remoteOptions, "remoteOptions must not be null");
if (actionContextProvider == null) {
return;
}
tempPathGenerator = getTempPathGenerator(env);
actionContextProvider.setTempPathGenerator(tempPathGenerator);
CoreOptions coreOptions = env.getOptions().getOptions(CoreOptions.class);
OutputPermissions outputPermissions =
coreOptions.experimentalWritableOutputs
? OutputPermissions.WRITABLE
: OutputPermissions.READONLY;
if (actionContextProvider.getRemoteCache() != null) {
Preconditions.checkNotNull(remoteOutputChecker, "remoteOutputChecker must not be null");
Preconditions.checkNotNull(outputService, "remoteOutputService must not be null");
actionInputFetcher =
new RemoteActionInputFetcher(
env.getReporter(),
env.getBuildRequestId(),
env.getCommandId().toString(),
actionContextProvider.getRemoteCache(),
env.getExecRoot(),
tempPathGenerator,
remoteOutputChecker,
env.getOutputDirectoryHelper(),
outputPermissions);
env.getEventBus().register(actionInputFetcher);
builder.setActionInputPrefetcher(actionInputFetcher);
actionContextProvider.setActionInputFetcher(actionInputFetcher);
LeaseExtension leaseExtension = null;
if (remoteOptions.remoteCacheLeaseExtension) {
leaseExtension =
new RemoteLeaseExtension(
env.getSkyframeExecutor().getEvaluator(),
env.getBlazeWorkspace().getPersistentActionCache(),
env.getBuildRequestId(),
env.getCommandId().toString(),
actionContextProvider.getRemoteCache(),
remoteOptions.remoteCacheTtl);
}
var leaseService =
new LeaseService(
env.getSkyframeExecutor().getEvaluator(),
env.getBlazeWorkspace().getPersistentActionCache(),
leaseExtension);
env.getEventBus().register(leaseService);
if (outputService instanceof RemoteOutputService remoteOutputService) {
remoteOutputService.setRemoteOutputChecker(remoteOutputChecker);
remoteOutputService.setActionInputFetcher(actionInputFetcher);
remoteOutputService.setLeaseService(leaseService);
remoteOutputService.setFileCacheSupplier(env::getFileCache);
env.getEventBus().register(outputService);
}
}
}
@Override
@Nullable
public OutputService getOutputService() {
return outputService;
}
@Override
public Iterable<Class<? extends OptionsBase>> getCommonCommandOptions() {
return ImmutableList.of(RemoteOptions.class, AuthAndTLSOptions.class);
}
private static class BuildEventArtifactUploaderFactoryDelegate
implements BuildEventArtifactUploaderFactory {
@Nullable private ByteStreamBuildEventArtifactUploaderFactory uploaderFactory;
public void init(ByteStreamBuildEventArtifactUploaderFactory uploaderFactory) {
Preconditions.checkState(this.uploaderFactory == null);
this.uploaderFactory = uploaderFactory;
}
@Nullable
public ByteStreamBuildEventArtifactUploader get() {
if (uploaderFactory == null) {
return null;
}
return uploaderFactory.get();
}
public void reset() {
this.uploaderFactory = null;
}
@Override
public BuildEventArtifactUploader create(CommandEnvironment env)
throws InvalidPackagePathSymlinkException {
BuildEventArtifactUploaderFactory uploaderFactory0 = this.uploaderFactory;
if (uploaderFactory0 == null) {
return new LocalFilesArtifactUploader();
}
return uploaderFactory0.create(env);
}
}
private static AbruptExitException createOptionsExitException(
String message, FailureDetails.RemoteOptions.Code remoteExecutionCode) {
return new AbruptExitException(
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setRemoteOptions(
FailureDetails.RemoteOptions.newBuilder().setCode(remoteExecutionCode))
.build()));
}
private static AbruptExitException createExitException(
String message, ExitCode exitCode, Code remoteExecutionCode) {
return new AbruptExitException(
DetailedExitCode.of(
exitCode,
FailureDetail.newBuilder()
.setMessage(message)
.setRemoteExecution(RemoteExecution.newBuilder().setCode(remoteExecutionCode))
.build()));
}
private static class RepositoryRemoteExecutorFactoryDelegate
implements RepositoryRemoteExecutorFactory {
private volatile RepositoryRemoteExecutorFactory delegate;
public void init(RepositoryRemoteExecutorFactory delegate) {
Preconditions.checkState(this.delegate == null);
this.delegate = delegate;
}
public void reset() {
this.delegate = null;
}
@Nullable
@Override
public RepositoryRemoteExecutor create() {
RepositoryRemoteExecutorFactory delegate = this.delegate;
if (delegate == null) {
return null;
}
return delegate.create();
}
}
@VisibleForTesting
void setChannelFactory(ChannelFactory channelFactory) {
this.channelFactory = channelFactory;
}
@VisibleForTesting
RemoteActionContextProvider getActionContextProvider() {
return actionContextProvider;
}
@VisibleForTesting
static Credentials createCredentials(
CredentialHelperEnvironment credentialHelperEnvironment,
Cache<URI, GetCredentialsResponse> credentialCache,
CommandLinePathFactory commandLinePathFactory,
FileSystem fileSystem,
AuthAndTLSOptions authAndTlsOptions,
RemoteOptions remoteOptions)
throws IOException {
Credentials credentials =
GoogleAuthUtils.newCredentials(
credentialHelperEnvironment,
credentialCache,
commandLinePathFactory,
fileSystem,
authAndTlsOptions);
try {
if (credentials != null
&& remoteOptions.remoteCache != null
&& Ascii.toLowerCase(remoteOptions.remoteCache).startsWith("http://")
&& !credentials.getRequestMetadata(new URI(remoteOptions.remoteCache)).isEmpty()) {
// TODO(yannic): Make this a error aborting the build.
credentialHelperEnvironment
.getEventReporter()
.handle(
Event.warn(
"Credentials are transmitted in plaintext to "
+ remoteOptions.remoteCache
+ ". Please consider using an HTTPS endpoint."));
}
} catch (URISyntaxException e) {
throw new IOException(e.getMessage(), e);
}
return credentials;
}
@VisibleForTesting
MutableSupplier<Downloader> getRemoteDownloaderSupplier() {
return remoteDownloaderSupplier;
}
}