| // Copyright 2018 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 build.bazel.remote.execution.v2.CacheCapabilities; | |
| import build.bazel.remote.execution.v2.CapabilitiesGrpc; | |
| import build.bazel.remote.execution.v2.CapabilitiesGrpc.CapabilitiesFutureStub; | |
| import build.bazel.remote.execution.v2.Compressor; | |
| import build.bazel.remote.execution.v2.DigestFunction; | |
| import build.bazel.remote.execution.v2.ExecutionCapabilities; | |
| import build.bazel.remote.execution.v2.GetCapabilitiesRequest; | |
| import build.bazel.remote.execution.v2.PriorityCapabilities; | |
| import build.bazel.remote.execution.v2.PriorityCapabilities.PriorityRange; | |
| import build.bazel.remote.execution.v2.RequestMetadata; | |
| import build.bazel.remote.execution.v2.ServerCapabilities; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.util.concurrent.ListenableFuture; | |
| import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext; | |
| import com.google.devtools.build.lib.remote.options.RemoteOptions; | |
| import com.google.devtools.build.lib.remote.util.TracingMetadataUtils; | |
| import io.grpc.CallCredentials; | |
| import io.grpc.Channel; | |
| import io.grpc.ManagedChannel; | |
| import java.util.List; | |
| import javax.annotation.Nullable; | |
| /** Fetches the ServerCapabilities of the remote execution/cache server. */ | |
| class RemoteServerCapabilities { | |
| private final String buildRequestId; | |
| private final String commandId; | |
| @Nullable private final String instanceName; | |
| @Nullable private final CallCredentials callCredentials; | |
| private final RemoteRetrier retrier; | |
| public RemoteServerCapabilities( | |
| String buildRequestId, | |
| String commandId, | |
| @Nullable String instanceName, | |
| @Nullable CallCredentials callCredentials, | |
| RemoteRetrier retrier) { | |
| this.buildRequestId = buildRequestId; | |
| this.commandId = commandId; | |
| this.instanceName = instanceName; | |
| this.callCredentials = callCredentials; | |
| this.retrier = retrier; | |
| } | |
| private CapabilitiesFutureStub capabilitiesFutureStub( | |
| RemoteActionExecutionContext context, Channel channel) { | |
| return CapabilitiesGrpc.newFutureStub(channel) | |
| .withInterceptors( | |
| TracingMetadataUtils.attachMetadataInterceptor(context.getRequestMetadata())) | |
| .withCallCredentials(callCredentials); | |
| } | |
| public ListenableFuture<ServerCapabilities> get(ManagedChannel channel) { | |
| RequestMetadata metadata = | |
| TracingMetadataUtils.buildMetadata(buildRequestId, commandId, "capabilities"); | |
| RemoteActionExecutionContext context = RemoteActionExecutionContext.create(metadata); | |
| GetCapabilitiesRequest request = | |
| instanceName == null | |
| ? GetCapabilitiesRequest.getDefaultInstance() | |
| : GetCapabilitiesRequest.newBuilder().setInstanceName(instanceName).build(); | |
| return retrier.executeAsync( | |
| () -> capabilitiesFutureStub(context, channel).getCapabilities(request)); | |
| } | |
| static class ClientServerCompatibilityStatus { | |
| private final List<String> warnings; | |
| private final List<String> errors; | |
| private ClientServerCompatibilityStatus(List<String> warnings, List<String> errors) { | |
| this.warnings = warnings; | |
| this.errors = errors; | |
| } | |
| static class Builder { | |
| private final ImmutableList.Builder<String> warnings = ImmutableList.builder(); | |
| private final ImmutableList.Builder<String> errors = ImmutableList.builder(); | |
| public void addWarning(String message) { | |
| warnings.add(message); | |
| } | |
| public void addError(String message) { | |
| errors.add(message); | |
| } | |
| public ClientServerCompatibilityStatus build() { | |
| return new ClientServerCompatibilityStatus(warnings.build(), errors.build()); | |
| } | |
| } | |
| public boolean isOk() { | |
| return warnings.isEmpty() && errors.isEmpty(); | |
| } | |
| public List<String> getWarnings() { | |
| return warnings; | |
| } | |
| public List<String> getErrors() { | |
| return errors; | |
| } | |
| } | |
| private static void checkPriorityInRange( | |
| int priority, | |
| String optionName, | |
| PriorityCapabilities prCap, | |
| ClientServerCompatibilityStatus.Builder result) { | |
| if (priority != 0) { | |
| boolean found = false; | |
| StringBuilder rangeBuilder = new StringBuilder(); | |
| for (PriorityRange pr : prCap.getPrioritiesList()) { | |
| rangeBuilder.append(String.format("%d-%d,", pr.getMinPriority(), pr.getMaxPriority())); | |
| if (pr.getMinPriority() <= priority && priority <= pr.getMaxPriority()) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| String range = rangeBuilder.toString(); | |
| if (!range.isEmpty()) { | |
| range = range.substring(0, range.length() - 1); | |
| } | |
| result.addError( | |
| String.format( | |
| "--%s %d is outside of server supported range %s.", optionName, priority, range)); | |
| } | |
| } | |
| } | |
| public enum ServerCapabilitiesRequirement { | |
| NONE, | |
| CACHE, | |
| EXECUTION, | |
| EXECUTION_AND_CACHE, | |
| } | |
| /** Compare the remote server capabilities with those requested by current execution. */ | |
| public static ClientServerCompatibilityStatus checkClientServerCompatibility( | |
| ServerCapabilities capabilities, | |
| RemoteOptions remoteOptions, | |
| DigestFunction.Value digestFunction, | |
| ServerCapabilitiesRequirement requirement) { | |
| ClientServerCompatibilityStatus.Builder result = new ClientServerCompatibilityStatus.Builder(); | |
| boolean shouldCheckExecutionCapabilities = | |
| (requirement == ServerCapabilitiesRequirement.EXECUTION | |
| || requirement == ServerCapabilitiesRequirement.EXECUTION_AND_CACHE); | |
| boolean shouldCheckCacheCapabilities = | |
| (requirement == ServerCapabilitiesRequirement.CACHE | |
| || requirement == ServerCapabilitiesRequirement.EXECUTION_AND_CACHE); | |
| if (!(shouldCheckCacheCapabilities || shouldCheckExecutionCapabilities)) { | |
| return result.build(); | |
| } | |
| // Check API version. | |
| ClientApiVersion.ServerSupportedStatus st = | |
| ClientApiVersion.current.checkServerSupportedVersions(capabilities); | |
| if (st.isUnsupported()) { | |
| result.addError(st.getMessage()); | |
| } | |
| if (st.isDeprecated()) { | |
| result.addWarning(st.getMessage()); | |
| } | |
| if (shouldCheckExecutionCapabilities) { | |
| // Check remote execution is enabled. | |
| ExecutionCapabilities execCap = capabilities.getExecutionCapabilities(); | |
| if (!execCap.getExecEnabled()) { | |
| result.addError( | |
| "Remote execution is not supported by the remote server, or the current " | |
| + "account is not authorized to use remote execution."); | |
| return result.build(); // No point checking other execution fields. | |
| } | |
| // Check execution digest function. The protocol only later added | |
| // support for multiple digest functions for remote execution, so | |
| // check both the singular and repeated field. | |
| if (execCap.getDigestFunctionsList().isEmpty() | |
| && execCap.getDigestFunction() != DigestFunction.Value.UNKNOWN) { | |
| if (execCap.getDigestFunction() != digestFunction) { | |
| result.addError( | |
| String.format( | |
| "Cannot use hash function %s with remote execution. " | |
| + "Server supported function is %s", | |
| digestFunction, execCap.getDigestFunction())); | |
| } | |
| } else if (!execCap.getDigestFunctionsList().contains(digestFunction)) { | |
| result.addError( | |
| String.format( | |
| "Cannot use hash function %s with remote execution. " | |
| + "Server supported functions are: %s", | |
| digestFunction, execCap.getDigestFunctionsList())); | |
| } | |
| // Check execution priority is in the supported range. | |
| checkPriorityInRange( | |
| remoteOptions.getRemoteExecutionPriority(), | |
| "remote_execution_priority", | |
| execCap.getExecutionPriorityCapabilities(), | |
| result); | |
| } | |
| if (shouldCheckCacheCapabilities) { | |
| // Check cache digest function. | |
| CacheCapabilities cacheCap = capabilities.getCacheCapabilities(); | |
| if (!cacheCap.getDigestFunctionsList().contains(digestFunction)) { | |
| result.addError( | |
| String.format( | |
| "Cannot use hash function %s with remote cache. " | |
| + "Server supported functions are: %s", | |
| digestFunction, cacheCap.getDigestFunctionsList())); | |
| } | |
| if (remoteOptions.getRemoteUploadLocalResults() | |
| && !cacheCap.getActionCacheUpdateCapabilities().getUpdateEnabled()) { | |
| result.addWarning( | |
| "--remote_upload_local_results is set, but the remote cache does not support uploading " | |
| + "action results or the current account is not authorized to write local results " | |
| + "to the remote cache."); | |
| } | |
| if (remoteOptions.getCacheCompression() | |
| && !cacheCap.getSupportedCompressorsList().contains(Compressor.Value.ZSTD)) { | |
| result.addError( | |
| "--remote_cache_compression requested but remote does not support compression"); | |
| } | |
| if (remoteOptions.getExperimentalRemoteCacheChunking()) { | |
| if (!cacheCap.getSplitBlobSupport()) { | |
| result.addError( | |
| "--experimental_remote_cache_chunking requested but remote does not support" | |
| + " SplitBlob"); | |
| } | |
| if (!cacheCap.getSpliceBlobSupport()) { | |
| result.addError( | |
| "--experimental_remote_cache_chunking requested but remote does not support" | |
| + " SpliceBlob"); | |
| } | |
| switch (remoteOptions.getExperimentalRemoteCacheChunkingFunction()) { | |
| case AUTO -> { | |
| if (!cacheCap.hasFastCdc2020Params() && !cacheCap.hasRepMaxCdcParams()) { | |
| result.addError( | |
| "--experimental_remote_cache_chunking requested but remote does not support" | |
| + " any chunking algorithm supported by Bazel (FastCDC 2020 or RepMaxCDC)"); | |
| } | |
| } | |
| case FAST_CDC_2020 -> { | |
| if (!cacheCap.hasFastCdc2020Params()) { | |
| result.addError( | |
| "--experimental_remote_cache_chunking requested but remote does not support" | |
| + " FastCDC 2020 chunking algorithm"); | |
| } | |
| } | |
| case REP_MAX_CDC -> { | |
| if (!cacheCap.hasRepMaxCdcParams()) { | |
| result.addError( | |
| "--experimental_remote_cache_chunking requested but remote does not support" | |
| + " RepMaxCDC chunking algorithm"); | |
| } | |
| } | |
| } | |
| } | |
| // Check result cache priority is in the supported range. | |
| checkPriorityInRange( | |
| remoteOptions.getRemoteResultCachePriority(), | |
| "remote_result_cache_priority", | |
| cacheCap.getCachePriorityCapabilities(), | |
| result); | |
| } | |
| return result.build(); | |
| } | |
| } |