| // 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 static com.google.common.truth.Truth.assertThat; |
| |
| import build.bazel.remote.execution.v2.ActionCacheUpdateCapabilities; |
| import build.bazel.remote.execution.v2.CacheCapabilities; |
| import build.bazel.remote.execution.v2.CapabilitiesGrpc.CapabilitiesImplBase; |
| 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.collect.Maps; |
| import com.google.common.util.concurrent.ListeningScheduledExecutorService; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.devtools.build.lib.analysis.BlazeVersionInfo; |
| import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions; |
| import com.google.devtools.build.lib.authandtls.GoogleAuthUtils; |
| import com.google.devtools.build.lib.remote.RemoteRetrier.ExponentialBackoff; |
| import com.google.devtools.build.lib.remote.options.RemoteOptions; |
| import com.google.devtools.build.lib.remote.util.TestUtils; |
| import com.google.devtools.build.lib.remote.util.TracingMetadataUtils; |
| import com.google.devtools.common.options.Options; |
| import io.grpc.CallCredentials; |
| import io.grpc.Metadata; |
| import io.grpc.Server; |
| import io.grpc.ServerCall; |
| import io.grpc.ServerCallHandler; |
| import io.grpc.ServerInterceptor; |
| import io.grpc.ServerInterceptors; |
| import io.grpc.Status; |
| import io.grpc.inprocess.InProcessChannelBuilder; |
| import io.grpc.inprocess.InProcessServerBuilder; |
| import io.grpc.stub.StreamObserver; |
| import io.grpc.util.MutableHandlerRegistry; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.TimeUnit; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Tests for {@link RemoteServerCapabilities}. */ |
| @RunWith(JUnit4.class) |
| public class RemoteServerCapabilitiesTest { |
| |
| private final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry(); |
| private final String fakeServerName = "fake server for " + getClass(); |
| private Server fakeServer; |
| private ListeningScheduledExecutorService retryService; |
| |
| @Before |
| public final void setUp() throws Exception { |
| fakeServer = |
| InProcessServerBuilder.forName(fakeServerName) |
| .fallbackHandlerRegistry(serviceRegistry) |
| .directExecutor() |
| .build() |
| .start(); |
| retryService = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1)); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| retryService.shutdownNow(); |
| retryService.awaitTermination( |
| com.google.devtools.build.lib.testutil.TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| |
| fakeServer.shutdownNow(); |
| fakeServer.awaitTermination(); |
| } |
| |
| /** Capture the request headers from a client. Useful for testing metadata propagation. */ |
| private static class RequestHeadersValidator implements ServerInterceptor { |
| @Override |
| public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( |
| ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { |
| RequestMetadata meta = headers.get(TracingMetadataUtils.METADATA_KEY); |
| assertThat(meta.getCorrelatedInvocationsId()).isEqualTo("build-req-id"); |
| assertThat(meta.getToolInvocationId()).isEqualTo("command-id"); |
| assertThat(meta.getActionId()).isNotEmpty(); |
| assertThat(meta.getToolDetails().getToolName()).isEqualTo("bazel"); |
| assertThat(meta.getToolDetails().getToolVersion()) |
| .isEqualTo(BlazeVersionInfo.instance().getVersion()); |
| return next.startCall(call, headers); |
| } |
| } |
| |
| private static class RequestCustomHeadersValidator implements ServerInterceptor { |
| @Override |
| public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( |
| ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { |
| assertThat(headers.get(Metadata.Key.of("Key1", Metadata.ASCII_STRING_MARSHALLER))) |
| .isEqualTo("Value1"); |
| assertThat(headers.get(Metadata.Key.of("Key2", Metadata.ASCII_STRING_MARSHALLER))) |
| .isEqualTo("Value2"); |
| return next.startCall(call, headers); |
| } |
| } |
| |
| @Test |
| public void testCustomHeadersAreAttached() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setExecutionCapabilities( |
| ExecutionCapabilities.newBuilder().setExecEnabled(true).build()) |
| .build(); |
| serviceRegistry.addService( |
| ServerInterceptors.intercept( |
| new CapabilitiesImplBase() { |
| @Override |
| public void getCapabilities( |
| GetCapabilitiesRequest request, |
| StreamObserver<ServerCapabilities> responseObserver) { |
| responseObserver.onNext(caps); |
| responseObserver.onCompleted(); |
| } |
| }, |
| new RequestCustomHeadersValidator())); |
| |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteHeaders = |
| ImmutableList.of( |
| Maps.immutableEntry("Key1", "Value1"), Maps.immutableEntry("Key2", "Value2")); |
| |
| RemoteRetrier retrier = |
| TestUtils.newRemoteRetrier( |
| () -> new ExponentialBackoff(remoteOptions), |
| RemoteRetrier.RETRIABLE_GRPC_ERRORS, |
| retryService); |
| ReferenceCountedChannel channel = |
| new ReferenceCountedChannel( |
| InProcessChannelBuilder.forName(fakeServerName) |
| .intercept(TracingMetadataUtils.newExecHeadersInterceptor(remoteOptions)) |
| .directExecutor() |
| .build()); |
| RemoteServerCapabilities client = |
| new RemoteServerCapabilities("instance", channel.retain(), null, 3, retrier); |
| |
| assertThat(client.get("build-req-id", "command-id")).isEqualTo(caps); |
| } |
| |
| @Test |
| public void testGetCapabilitiesWithRetries() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setExecutionCapabilities( |
| ExecutionCapabilities.newBuilder().setExecEnabled(true).build()) |
| .build(); |
| serviceRegistry.addService( |
| ServerInterceptors.intercept( |
| new CapabilitiesImplBase() { |
| private int numErrors = 0; |
| private static final int MAX_ERRORS = 3; |
| |
| @Override |
| public void getCapabilities( |
| GetCapabilitiesRequest request, |
| StreamObserver<ServerCapabilities> responseObserver) { |
| if (numErrors < MAX_ERRORS) { |
| numErrors++; |
| responseObserver.onError( |
| Status.UNAVAILABLE.asRuntimeException()); // Retriable error. |
| } else { |
| responseObserver.onNext(caps); |
| responseObserver.onCompleted(); |
| } |
| } |
| }, |
| new RequestHeadersValidator())); |
| |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| RemoteRetrier retrier = |
| TestUtils.newRemoteRetrier( |
| () -> new ExponentialBackoff(remoteOptions), |
| RemoteRetrier.RETRIABLE_GRPC_ERRORS, |
| retryService); |
| ReferenceCountedChannel channel = |
| new ReferenceCountedChannel( |
| InProcessChannelBuilder.forName(fakeServerName).directExecutor().build()); |
| CallCredentials creds = |
| GoogleAuthUtils.newCallCredentials(Options.getDefaults(AuthAndTLSOptions.class)); |
| RemoteServerCapabilities client = |
| new RemoteServerCapabilities("instance", channel.retain(), creds, 3, retrier); |
| |
| assertThat(client.get("build-req-id", "command-id")).isEqualTo(caps); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_NoChecks() throws Exception { |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| ServerCapabilities.getDefaultInstance(), |
| Options.getDefaults(RemoteOptions.class), |
| DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_ApiVersionDeprecated() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setDeprecatedApiVersion(ApiVersion.current.toSemVer()) |
| .setLowApiVersion(new ApiVersion(100, 0, 0, "").toSemVer()) |
| .setHighApiVersion(new ApiVersion(100, 0, 0, "").toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .setActionCacheUpdateCapabilities( |
| ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteCache = "server:port"; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).isEmpty(); |
| assertThat(st.getWarnings()).hasSize(1); |
| assertThat(st.getWarnings().get(0)).containsMatch("API.*deprecated.*100.0"); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_ApiVersionUnsupported() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(new ApiVersion(100, 0, 0, "").toSemVer()) |
| .setHighApiVersion(new ApiVersion(100, 0, 0, "").toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .setActionCacheUpdateCapabilities( |
| ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteCache = "server:port"; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)).containsMatch("API.*not supported.*100.0"); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_RemoteCacheDoesNotSupportDigestFunction() |
| throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.MD5) |
| .setActionCacheUpdateCapabilities( |
| ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteCache = "server:port"; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)).containsMatch("Cannot use hash function"); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_RemoteCacheDoesNotSupportUpdate() |
| throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteCache = "server:port"; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)) |
| .containsMatch("not authorized to write local results to the remote cache"); |
| |
| // Ignored when no local upload. |
| remoteOptions.remoteUploadLocalResults = false; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_RemoteExecutionIsDisabled() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .setActionCacheUpdateCapabilities( |
| ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) |
| .build()) |
| .setExecutionCapabilities( |
| ExecutionCapabilities.newBuilder() |
| .setDigestFunction(DigestFunction.Value.SHA256) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteExecutor = "server:port"; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)).containsMatch("Remote execution is not supported"); |
| assertThat(st.getErrors().get(0)).containsMatch("not authorized to use remote execution"); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_RemoteExecutionDoesNotSupportDigestFunction() |
| throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .setActionCacheUpdateCapabilities( |
| ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) |
| .build()) |
| .setExecutionCapabilities( |
| ExecutionCapabilities.newBuilder() |
| .setDigestFunction(DigestFunction.Value.MD5) |
| .setExecEnabled(true) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteExecutor = "server:port"; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)).containsMatch("Cannot use hash function"); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_LocalFallbackNoRemoteCacheUpdate() |
| throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .build()) |
| .setExecutionCapabilities( |
| ExecutionCapabilities.newBuilder() |
| .setDigestFunction(DigestFunction.Value.SHA256) |
| .setExecEnabled(true) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteExecutor = "server:port"; |
| remoteOptions.remoteLocalFallback = true; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)) |
| .containsMatch("not authorized to write local results to the remote cache"); |
| |
| // Ignored when no fallback. |
| remoteOptions.remoteLocalFallback = false; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| |
| // Ignored when no uploading local results. |
| remoteOptions.remoteLocalFallback = true; |
| remoteOptions.remoteUploadLocalResults = false; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_CachePriority() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .setCachePriorityCapabilities( |
| PriorityCapabilities.newBuilder() |
| .addPriorities( |
| PriorityRange.newBuilder().setMinPriority(1).setMaxPriority(2)) |
| .addPriorities( |
| PriorityRange.newBuilder().setMinPriority(5).setMaxPriority(10))) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteCache = "server:port"; |
| remoteOptions.remoteUploadLocalResults = false; |
| remoteOptions.remoteResultCachePriority = 11; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)).containsMatch("remote_result_cache_priority"); |
| |
| // Valid value in range. |
| remoteOptions.remoteResultCachePriority = 10; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| |
| // Check not performed if the value is 0. |
| remoteOptions.remoteResultCachePriority = 0; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| } |
| |
| @Test |
| public void testCheckClientServerCompatibility_ExecutionPriority() throws Exception { |
| ServerCapabilities caps = |
| ServerCapabilities.newBuilder() |
| .setLowApiVersion(ApiVersion.current.toSemVer()) |
| .setHighApiVersion(ApiVersion.current.toSemVer()) |
| .setCacheCapabilities( |
| CacheCapabilities.newBuilder() |
| .addDigestFunction(DigestFunction.Value.SHA256) |
| .build()) |
| .setExecutionCapabilities( |
| ExecutionCapabilities.newBuilder() |
| .setDigestFunction(DigestFunction.Value.SHA256) |
| .setExecEnabled(true) |
| .setExecutionPriorityCapabilities( |
| PriorityCapabilities.newBuilder() |
| .addPriorities( |
| PriorityRange.newBuilder().setMinPriority(1).setMaxPriority(2)) |
| .addPriorities( |
| PriorityRange.newBuilder().setMinPriority(5).setMaxPriority(10))) |
| .build()) |
| .build(); |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); |
| remoteOptions.remoteExecutor = "server:port"; |
| remoteOptions.remoteUploadLocalResults = false; |
| remoteOptions.remoteExecutionPriority = 11; |
| RemoteServerCapabilities.ClientServerCompatibilityStatus st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.getErrors()).hasSize(1); |
| assertThat(st.getErrors().get(0)).containsMatch("remote_execution_priority"); |
| |
| // Valid value in range. |
| remoteOptions.remoteExecutionPriority = 10; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| |
| // Check not performed if the value is 0. |
| remoteOptions.remoteExecutionPriority = 0; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| |
| // Ignored when no remote execution requested. |
| remoteOptions.remoteExecutionPriority = 11; |
| remoteOptions.remoteExecutor = ""; |
| remoteOptions.remoteCache = "server:port"; |
| st = |
| RemoteServerCapabilities.checkClientServerCompatibility( |
| caps, remoteOptions, DigestFunction.Value.SHA256); |
| assertThat(st.isOk()).isTrue(); |
| } |
| } |