diff --git a/bom/build.gradle b/bom/build.gradle index 43668511e3..26ee7fbe51 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -12,7 +12,7 @@ publishing { pom.withXml { // Generate bom using subprojects - def internalProjects = [project.name, 'grpc-gae-interop-testing-jdk8', 'grpc-compiler'] + def internalProjects = [project.name, 'grpc-gae-interop-testing-jdk8', 'grpc-compiler', 'grpc-rls'] def dependencyManagement = asNode().appendNode('dependencyManagement') def dependencies = dependencyManagement.appendNode('dependencies') diff --git a/buildscripts/sync-protos.sh b/buildscripts/sync-protos.sh index ad3c8d597e..968147ccac 100755 --- a/buildscripts/sync-protos.sh +++ b/buildscripts/sync-protos.sh @@ -8,7 +8,7 @@ curl -Ls https://github.com/grpc/grpc-proto/archive/master.tar.gz | tar xz -C "$ base="$tmpdir/grpc-proto-master" # Copy protos in 'src/main/proto' from grpc-proto for these projects -for project in alts grpclb services; do +for project in alts grpclb services rls; do while read -r proto; do [ -f "$base/$proto" ] && cp "$base/$proto" "$project/src/main/proto/$proto" echo "$proto" diff --git a/core/src/main/java/io/grpc/internal/JsonUtil.java b/core/src/main/java/io/grpc/internal/JsonUtil.java index fabc08d646..d80b4ed44c 100644 --- a/core/src/main/java/io/grpc/internal/JsonUtil.java +++ b/core/src/main/java/io/grpc/internal/JsonUtil.java @@ -129,6 +129,24 @@ public class JsonUtil { return i; } + /** + * Gets a number from an object for the given key, casted to an long. If the key is not + * present, this returns null. If the value is not a Double or loses precision when cast to an + * long, throws an exception. + */ + public static Long getNumberAsLong(Map obj, String key) { + Double d = getNumber(obj, key); + if (d == null) { + return null; + } + long l = d.longValue(); + if (l != d) { + throw new ClassCastException("Number expected to be long: " + d); + } + return l; + } + + /** * Gets a string from an object for the given key. If the key is not present, this returns null. * If the value is not a String, throws an exception. diff --git a/rls/build.gradle b/rls/build.gradle new file mode 100644 index 0000000000..93270fc534 --- /dev/null +++ b/rls/build.gradle @@ -0,0 +1,22 @@ +plugins { + id "java" + id "maven-publish" + id "com.google.protobuf" +} + +description = "gRPC: RouteLookupService Loadbalancing plugin" + +evaluationDependsOn(project(':grpc-core').path) + +dependencies { + implementation project(':grpc-core'), + project(':grpc-protobuf'), + project(':grpc-stub') + compileOnly libraries.javax_annotation + testCompile libraries.truth +} + +configureProtoCompilation() + +// do not publish 'grpc-rls' +[publishMavenPublicationToMavenRepository]*.onlyIf { false } diff --git a/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java new file mode 100644 index 0000000000..64b2ae3535 --- /dev/null +++ b/rls/src/generated/main/grpc/io/grpc/lookup/v1/RouteLookupServiceGrpc.java @@ -0,0 +1,300 @@ +package io.grpc.lookup.v1; + +import static io.grpc.MethodDescriptor.generateFullMethodName; +import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ClientCalls.asyncClientStreamingCall; +import static io.grpc.stub.ClientCalls.asyncServerStreamingCall; +import static io.grpc.stub.ClientCalls.asyncUnaryCall; +import static io.grpc.stub.ClientCalls.blockingServerStreamingCall; +import static io.grpc.stub.ClientCalls.blockingUnaryCall; +import static io.grpc.stub.ClientCalls.futureUnaryCall; +import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ServerCalls.asyncClientStreamingCall; +import static io.grpc.stub.ServerCalls.asyncServerStreamingCall; +import static io.grpc.stub.ServerCalls.asyncUnaryCall; +import static io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall; +import static io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall; + +/** + */ +@javax.annotation.Generated( + value = "by gRPC proto compiler", + comments = "Source: grpc/lookup/v1/rls.proto") +public final class RouteLookupServiceGrpc { + + private RouteLookupServiceGrpc() {} + + public static final String SERVICE_NAME = "grpc.lookup.v1.RouteLookupService"; + + // Static method descriptors that strictly reflect the proto. + private static volatile io.grpc.MethodDescriptor getRouteLookupMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "RouteLookup", + requestType = io.grpc.lookup.v1.RouteLookupRequest.class, + responseType = io.grpc.lookup.v1.RouteLookupResponse.class, + methodType = io.grpc.MethodDescriptor.MethodType.UNARY) + public static io.grpc.MethodDescriptor getRouteLookupMethod() { + io.grpc.MethodDescriptor getRouteLookupMethod; + if ((getRouteLookupMethod = RouteLookupServiceGrpc.getRouteLookupMethod) == null) { + synchronized (RouteLookupServiceGrpc.class) { + if ((getRouteLookupMethod = RouteLookupServiceGrpc.getRouteLookupMethod) == null) { + RouteLookupServiceGrpc.getRouteLookupMethod = getRouteLookupMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "RouteLookup")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.grpc.lookup.v1.RouteLookupRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.grpc.lookup.v1.RouteLookupResponse.getDefaultInstance())) + .setSchemaDescriptor(new RouteLookupServiceMethodDescriptorSupplier("RouteLookup")) + .build(); + } + } + } + return getRouteLookupMethod; + } + + /** + * Creates a new async stub that supports all call types for the service + */ + public static RouteLookupServiceStub newStub(io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public RouteLookupServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceStub(channel, callOptions); + } + }; + return RouteLookupServiceStub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static RouteLookupServiceBlockingStub newBlockingStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public RouteLookupServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceBlockingStub(channel, callOptions); + } + }; + return RouteLookupServiceBlockingStub.newStub(factory, channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary calls on the service + */ + public static RouteLookupServiceFutureStub newFutureStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public RouteLookupServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceFutureStub(channel, callOptions); + } + }; + return RouteLookupServiceFutureStub.newStub(factory, channel); + } + + /** + */ + public static abstract class RouteLookupServiceImplBase implements io.grpc.BindableService { + + /** + *
+     * Lookup returns a target for a single key.
+     * 
+ */ + public void routeLookup(io.grpc.lookup.v1.RouteLookupRequest request, + io.grpc.stub.StreamObserver responseObserver) { + asyncUnimplementedUnaryCall(getRouteLookupMethod(), responseObserver); + } + + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + getRouteLookupMethod(), + asyncUnaryCall( + new MethodHandlers< + io.grpc.lookup.v1.RouteLookupRequest, + io.grpc.lookup.v1.RouteLookupResponse>( + this, METHODID_ROUTE_LOOKUP))) + .build(); + } + } + + /** + */ + public static final class RouteLookupServiceStub extends io.grpc.stub.AbstractAsyncStub { + private RouteLookupServiceStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected RouteLookupServiceStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceStub(channel, callOptions); + } + + /** + *
+     * Lookup returns a target for a single key.
+     * 
+ */ + public void routeLookup(io.grpc.lookup.v1.RouteLookupRequest request, + io.grpc.stub.StreamObserver responseObserver) { + asyncUnaryCall( + getChannel().newCall(getRouteLookupMethod(), getCallOptions()), request, responseObserver); + } + } + + /** + */ + public static final class RouteLookupServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { + private RouteLookupServiceBlockingStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected RouteLookupServiceBlockingStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceBlockingStub(channel, callOptions); + } + + /** + *
+     * Lookup returns a target for a single key.
+     * 
+ */ + public io.grpc.lookup.v1.RouteLookupResponse routeLookup(io.grpc.lookup.v1.RouteLookupRequest request) { + return blockingUnaryCall( + getChannel(), getRouteLookupMethod(), getCallOptions(), request); + } + } + + /** + */ + public static final class RouteLookupServiceFutureStub extends io.grpc.stub.AbstractFutureStub { + private RouteLookupServiceFutureStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected RouteLookupServiceFutureStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new RouteLookupServiceFutureStub(channel, callOptions); + } + + /** + *
+     * Lookup returns a target for a single key.
+     * 
+ */ + public com.google.common.util.concurrent.ListenableFuture routeLookup( + io.grpc.lookup.v1.RouteLookupRequest request) { + return futureUnaryCall( + getChannel().newCall(getRouteLookupMethod(), getCallOptions()), request); + } + } + + private static final int METHODID_ROUTE_LOOKUP = 0; + + private static final class MethodHandlers implements + io.grpc.stub.ServerCalls.UnaryMethod, + io.grpc.stub.ServerCalls.ServerStreamingMethod, + io.grpc.stub.ServerCalls.ClientStreamingMethod, + io.grpc.stub.ServerCalls.BidiStreamingMethod { + private final RouteLookupServiceImplBase serviceImpl; + private final int methodId; + + MethodHandlers(RouteLookupServiceImplBase serviceImpl, int methodId) { + this.serviceImpl = serviceImpl; + this.methodId = methodId; + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public void invoke(Req request, io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + case METHODID_ROUTE_LOOKUP: + serviceImpl.routeLookup((io.grpc.lookup.v1.RouteLookupRequest) request, + (io.grpc.stub.StreamObserver) responseObserver); + break; + default: + throw new AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver invoke( + io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + default: + throw new AssertionError(); + } + } + } + + private static abstract class RouteLookupServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { + RouteLookupServiceBaseDescriptorSupplier() {} + + @java.lang.Override + public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { + return io.grpc.lookup.v1.RlsProto.getDescriptor(); + } + + @java.lang.Override + public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { + return getFileDescriptor().findServiceByName("RouteLookupService"); + } + } + + private static final class RouteLookupServiceFileDescriptorSupplier + extends RouteLookupServiceBaseDescriptorSupplier { + RouteLookupServiceFileDescriptorSupplier() {} + } + + private static final class RouteLookupServiceMethodDescriptorSupplier + extends RouteLookupServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { + private final String methodName; + + RouteLookupServiceMethodDescriptorSupplier(String methodName) { + this.methodName = methodName; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() { + return getServiceDescriptor().findMethodByName(methodName); + } + } + + private static volatile io.grpc.ServiceDescriptor serviceDescriptor; + + public static io.grpc.ServiceDescriptor getServiceDescriptor() { + io.grpc.ServiceDescriptor result = serviceDescriptor; + if (result == null) { + synchronized (RouteLookupServiceGrpc.class) { + result = serviceDescriptor; + if (result == null) { + serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) + .setSchemaDescriptor(new RouteLookupServiceFileDescriptorSupplier()) + .addMethod(getRouteLookupMethod()) + .build(); + } + } + } + return result; + } +} diff --git a/rls/src/main/java/io/grpc/rls/internal/RlsProtoConverters.java b/rls/src/main/java/io/grpc/rls/internal/RlsProtoConverters.java new file mode 100644 index 0000000000..401a1930e9 --- /dev/null +++ b/rls/src/main/java/io/grpc/rls/internal/RlsProtoConverters.java @@ -0,0 +1,180 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc.rls.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Converter; +import io.grpc.internal.JsonUtil; +import io.grpc.lookup.v1.RouteLookupRequest; +import io.grpc.lookup.v1.RouteLookupResponse; +import io.grpc.rls.internal.RlsProtoData.GrpcKeyBuilder; +import io.grpc.rls.internal.RlsProtoData.GrpcKeyBuilder.Name; +import io.grpc.rls.internal.RlsProtoData.NameMatcher; +import io.grpc.rls.internal.RlsProtoData.RequestProcessingStrategy; +import io.grpc.rls.internal.RlsProtoData.RouteLookupConfig; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * RlsProtoConverters is a collection of {@link Converter} between RouteLookupService proto / json + * messages to internal representation in {@link RlsProtoData}. + */ +public final class RlsProtoConverters { + + /** + * RouteLookupRequestConverter converts between {@link RouteLookupRequest} and {@link + * RlsProtoData.RouteLookupRequest}. + */ + public static final class RouteLookupRequestConverter + extends Converter { + + @Override + protected RlsProtoData.RouteLookupRequest doForward(RouteLookupRequest routeLookupRequest) { + return + new RlsProtoData.RouteLookupRequest( + /* server= */ routeLookupRequest.getServer(), + /* path= */ routeLookupRequest.getPath(), + /* targetType= */ routeLookupRequest.getTargetType(), + routeLookupRequest.getKeyMapMap()); + } + + @Override + protected RouteLookupRequest doBackward(RlsProtoData.RouteLookupRequest routeLookupRequest) { + return + RouteLookupRequest.newBuilder() + .setServer(routeLookupRequest.getServer()) + .setPath(routeLookupRequest.getPath()) + .setTargetType(routeLookupRequest.getTargetType()) + .putAllKeyMap(routeLookupRequest.getKeyMap()) + .build(); + } + } + + /** + * RouteLookupResponseConverter converts between {@link RouteLookupResponse} and {@link + * RlsProtoData.RouteLookupResponse}. + */ + public static final class RouteLookupResponseConverter + extends Converter { + + @Override + protected RlsProtoData.RouteLookupResponse doForward(RouteLookupResponse routeLookupResponse) { + return + new RlsProtoData.RouteLookupResponse( + routeLookupResponse.getTarget(), + routeLookupResponse.getHeaderData()); + } + + @Override + protected RouteLookupResponse doBackward(RlsProtoData.RouteLookupResponse routeLookupResponse) { + return RouteLookupResponse.newBuilder() + .setTarget(routeLookupResponse.getTarget()) + .setHeaderData(routeLookupResponse.getHeaderData()) + .build(); + } + } + + /** + * RouteLookupConfigConverter converts between json map to {@link RouteLookupConfig}. + */ + public static final class RouteLookupConfigConverter + extends Converter, RouteLookupConfig> { + + @Override + protected RouteLookupConfig doForward(Map json) { + List grpcKeyBuilders = + GrpcKeyBuilderConverter + .covertAll(JsonUtil.checkObjectList(JsonUtil.getList(json, "grpcKeyBuilders"))); + String lookupService = JsonUtil.getString(json, "lookupService"); + long timeout = + TimeUnit.SECONDS.toMillis(JsonUtil.getNumberAsLong(json, "lookupServiceTimeout")); + Long maxAge = + convertTimeIfNotNull( + TimeUnit.SECONDS, TimeUnit.MILLISECONDS, JsonUtil.getNumberAsLong(json, "maxAge")); + Long staleAge = + convertTimeIfNotNull( + TimeUnit.SECONDS, TimeUnit.MILLISECONDS, JsonUtil.getNumberAsLong(json, "staleAge")); + long cacheSize = JsonUtil.getNumberAsLong(json, "cacheSizeBytes"); + List validTargets = JsonUtil.checkStringList(JsonUtil.getList(json, "validTargets")); + String defaultTarget = JsonUtil.getString(json, "defaultTarget"); + RequestProcessingStrategy strategy = + RequestProcessingStrategy + .valueOf(JsonUtil.getString(json, "requestProcessingStrategy").toUpperCase()); + return new RouteLookupConfig( + grpcKeyBuilders, + lookupService, + /* lookupServiceTimeoutInMillis= */ timeout, + /* maxAgeInMillis= */ maxAge, + /* staleAgeInMillis= */ staleAge, + /* cacheSizeBytes= */ cacheSize, + validTargets, + defaultTarget, + strategy); + } + + private static Long convertTimeIfNotNull(TimeUnit from, TimeUnit to, Long value) { + if (value == null) { + return null; + } + return to.convert(value, from); + } + + @Override + protected Map doBackward(RouteLookupConfig routeLookupConfig) { + throw new UnsupportedOperationException(); + } + } + + private static final class GrpcKeyBuilderConverter { + public static List covertAll(List> keyBuilders) { + List keyBuilderList = new ArrayList<>(); + for (Map keyBuilder : keyBuilders) { + keyBuilderList.add(convert(keyBuilder)); + } + return keyBuilderList; + } + + @SuppressWarnings("unchecked") + public static GrpcKeyBuilder convert(Map keyBuilder) { + List> rawNames = + JsonUtil.checkObjectList(JsonUtil.getList(keyBuilder, "names")); + List names = new ArrayList<>(); + for (Map rawName : rawNames) { + names.add( + new Name( + JsonUtil.getString(rawName, "service"), JsonUtil.getString(rawName, "method"))); + } + List> rawHeaders = + JsonUtil.checkObjectList(JsonUtil.getList(keyBuilder, "headers")); + List nameMatchers = new ArrayList<>(); + for (Map rawHeader : rawHeaders) { + NameMatcher matcher = + new NameMatcher( + JsonUtil.getString(rawHeader, "key"), + (List) rawHeader.get("names"), + (Boolean) rawHeader.get("optional")); + checkArgument( + matcher.isOptional(), "NameMatcher for GrpcKeyBuilders shouldn't be required"); + nameMatchers.add(matcher); + } + return new GrpcKeyBuilder(names, nameMatchers); + } + } +} diff --git a/rls/src/main/java/io/grpc/rls/internal/RlsProtoData.java b/rls/src/main/java/io/grpc/rls/internal/RlsProtoData.java new file mode 100644 index 0000000000..2e03f25899 --- /dev/null +++ b/rls/src/main/java/io/grpc/rls/internal/RlsProtoData.java @@ -0,0 +1,595 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc.rls.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.grpc.rls.internal.RlsProtoData.GrpcKeyBuilder.Name; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** RlsProtoData is a collection of internal representation of RouteLookupService proto messages. */ +public final class RlsProtoData { + + /** A request object sent to route lookup service. */ + @Immutable + public static final class RouteLookupRequest { + + private final String server; + + private final String path; + + private final String targetType; + + private final ImmutableMap keyMap; + + /** Constructor for RouteLookupRequest. */ + public RouteLookupRequest( + String server, String path, String targetType, Map keyMap) { + this.server = checkNotNull(server, "server"); + this.path = checkNotNull(path, "path"); + this.targetType = checkNotNull(targetType, "targetName"); + this.keyMap = ImmutableMap.copyOf(checkNotNull(keyMap, "keyMap")); + } + + /** + * Returns a full host name of the target server, {@literal e.g.} firestore.googleapis.com. Only + * set for gRPC requests; HTTP requests must use key_map explicitly. + */ + public String getServer() { + return server; + } + + /** + * Returns a full path of the request, {@literal i.e.} "/service/method". Only set for gRPC + * requests; HTTP requests must use key_map explicitly. + */ + public String getPath() { + return path; + } + + /** + * Returns the target type allows the client to specify what kind of target format it would like + * from RLS to allow it to find the regional server, {@literal e.g.} "grpc". + */ + public String getTargetType() { + return targetType; + } + + /** Returns a map of key values extracted via key builders for the gRPC or HTTP request. */ + public ImmutableMap getKeyMap() { + return keyMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RouteLookupRequest that = (RouteLookupRequest) o; + return Objects.equal(server, that.server) + && Objects.equal(path, that.path) + && Objects.equal(targetType, that.targetType) + && Objects.equal(keyMap, that.keyMap); + } + + @Override + public int hashCode() { + return Objects.hashCode(server, path, targetType, keyMap); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("server", server) + .add("path", path) + .add("targetName", targetType) + .add("keyMap", keyMap) + .toString(); + } + } + + /** A response from route lookup service. */ + @Immutable + public static final class RouteLookupResponse { + + private final String target; + + private final String headerData; + + public RouteLookupResponse(String target, String headerData) { + this.target = checkNotNull(target, "target"); + this.headerData = checkNotNull(headerData, "headerData"); + } + + /** + * Returns target. A target is an actual addressable entity to use for routing decision, using + * syntax requested by the request target_type. + */ + public String getTarget() { + return target; + } + + /** + * Returns optional header data to pass along to AFE in the X-Google-RLS-Data header. Cached + * with "target" and sent with all requests that match the request key. Allows the RLS to pass + * its work product to the eventual target. + */ + public String getHeaderData() { + return headerData; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RouteLookupResponse that = (RouteLookupResponse) o; + return java.util.Objects.equals(target, that.target) + && java.util.Objects.equals(headerData, that.headerData); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(target, headerData); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("target", target) + .add("headerData", headerData) + .toString(); + } + } + + /** A config object for gRPC RouteLookupService. */ + @Immutable + public static final class RouteLookupConfig { + + private static final long MAX_AGE_MILLIS = TimeUnit.MINUTES.toMillis(5); + + private final ImmutableList grpcKeyBuilders; + + private final String lookupService; + + private final long lookupServiceTimeoutInMillis; + + private final long maxAgeInMillis; + + private final long staleAgeInMillis; + + private final long cacheSizeBytes; + + private final ImmutableList validTargets; + + private final String defaultTarget; + + private final RequestProcessingStrategy requestProcessingStrategy; + + /** Constructs RouteLookupConfig. */ + public RouteLookupConfig( + List grpcKeyBuilders, + String lookupService, + long lookupServiceTimeoutInMillis, + @Nullable Long maxAgeInMillis, + @Nullable Long staleAgeInMillis, + long cacheSizeBytes, + List validTargets, + String defaultTarget, + RequestProcessingStrategy requestProcessingStrategy) { + checkState( + !checkNotNull(grpcKeyBuilders, "grpcKeyBuilders").isEmpty(), + "must have at least one GrpcKeyBuilder"); + checkUniqueName(grpcKeyBuilders); + this.grpcKeyBuilders = ImmutableList.copyOf(grpcKeyBuilders); + // TODO(creamsoup) also check if it is URI + checkState( + lookupService != null && !lookupService.isEmpty(), "lookupService must not be empty"); + this.lookupService = lookupService; + this.lookupServiceTimeoutInMillis = lookupServiceTimeoutInMillis; + if (maxAgeInMillis == null) { + checkState( + staleAgeInMillis == null, "To specify staleAgeInMillis, must have maxAgeInMillis"); + } + if (maxAgeInMillis == null || maxAgeInMillis == 0) { + maxAgeInMillis = MAX_AGE_MILLIS; + } + if (staleAgeInMillis == null || staleAgeInMillis == 0) { + staleAgeInMillis = MAX_AGE_MILLIS; + } + this.maxAgeInMillis = Math.min(maxAgeInMillis, MAX_AGE_MILLIS); + this.staleAgeInMillis = Math.min(staleAgeInMillis, this.maxAgeInMillis); + checkArgument(cacheSizeBytes > 0, "cacheSize must be positive"); + this.cacheSizeBytes = cacheSizeBytes; + this.validTargets = ImmutableList.copyOf(checkNotNull(validTargets, "validTargets")); + this.defaultTarget = checkNotNull(defaultTarget, "defaultTarget"); + this.requestProcessingStrategy = requestProcessingStrategy; + checkNotNull(requestProcessingStrategy, "requestProcessingStrategy"); + checkState( + (requestProcessingStrategy == RequestProcessingStrategy.SYNC_LOOKUP_CLIENT_SEES_ERROR + || requestProcessingStrategy + == RequestProcessingStrategy.ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS) + && !defaultTarget.isEmpty(), + "defaultTarget cannot be empty if strategy is %s", + requestProcessingStrategy); + } + + /** + * Returns unordered specifications for constructing keys for gRPC requests. All GrpcKeyBuilders + * on this list must have unique "name" fields so that the client is free to prebuild a hash map + * keyed by name. If no GrpcKeyBuilder matches, an empty key_map will be sent to the lookup + * service; it should likely reply with a global default route and raise an alert. + */ + public ImmutableList getGrpcKeyBuilders() { + return grpcKeyBuilders; + } + + /** + * Returns the name of the lookup service as a gRPC URI. Typically, this will be a subdomain of + * the target, such as "lookup.datastore.googleapis.com". + */ + public String getLookupService() { + return lookupService; + } + + /** Returns the timeout value for lookup service requests. */ + public long getLookupServiceTimeoutInMillis() { + return lookupServiceTimeoutInMillis; + } + + + /** Returns the maximum age the result will be cached. */ + public long getMaxAgeInMillis() { + return maxAgeInMillis; + } + + /** + * Returns the time when an entry will be in a staled status. When cache is accessed whgen the + * entry is in staled status, it will + */ + public long getStaleAgeInMillis() { + return staleAgeInMillis; + } + + /** + * Returns a rough indicator of amount of memory to use for the client cache. Some of the data + * structure overhead is not accounted for, so actual memory consumed will be somewhat greater + * than this value. If this field is omitted or set to zero, a client default will be used. + * The value may be capped to a lower amount based on client configuration. + */ + public long getCacheSizeBytes() { + return cacheSizeBytes; + } + + /** + * Returns the list of all the possible targets that can be returned by the lookup service. If + * a target not on this list is returned, it will be treated the same as an RPC error from the + * RLS. + */ + public ImmutableList getValidTargets() { + return validTargets; + } + + /** + * Returns the default target to use. It will be used for request processing strategy + * {@link RequestProcessingStrategy#SYNC_LOOKUP_DEFAULT_TARGET_ON_ERROR} if RLS + * returns an error, or strategy {@link + * RequestProcessingStrategy#ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS} if RLS returns an error or + * there is a cache miss in the client. It will also be used if there are no healthy backends + * for an RLS target. Note that requests can be routed only to a subdomain of the original + * target, {@literal e.g.} "us_east_1.cloudbigtable.googleapis.com". + */ + public String getDefaultTarget() { + return defaultTarget; + } + + /** Returns {@link RequestProcessingStrategy} to process RLS response. */ + public RequestProcessingStrategy getRequestProcessingStrategy() { + return requestProcessingStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RouteLookupConfig that = (RouteLookupConfig) o; + return lookupServiceTimeoutInMillis == that.lookupServiceTimeoutInMillis + && maxAgeInMillis == that.maxAgeInMillis + && staleAgeInMillis == that.staleAgeInMillis + && cacheSizeBytes == that.cacheSizeBytes + && Objects.equal(grpcKeyBuilders, that.grpcKeyBuilders) + && Objects.equal(lookupService, that.lookupService) + && Objects.equal(defaultTarget, that.defaultTarget) + && requestProcessingStrategy == that.requestProcessingStrategy; + } + + @Override + public int hashCode() { + return Objects.hashCode( + grpcKeyBuilders, + lookupService, + lookupServiceTimeoutInMillis, + maxAgeInMillis, + staleAgeInMillis, + cacheSizeBytes, + defaultTarget, + requestProcessingStrategy); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("grpcKeyBuilders", grpcKeyBuilders) + .add("lookupService", lookupService) + .add("lookupServiceTimeoutInMillis", lookupServiceTimeoutInMillis) + .add("maxAgeInMillis", maxAgeInMillis) + .add("staleAgeInMillis", staleAgeInMillis) + .add("cacheSize", cacheSizeBytes) + .add("defaultTarget", defaultTarget) + .add("requestProcessingStrategy", requestProcessingStrategy) + .toString(); + } + } + + private static void checkUniqueName(List grpcKeyBuilders) { + Set names = new HashSet<>(); + for (GrpcKeyBuilder grpcKeyBuilder : grpcKeyBuilders) { + int prevSize = names.size(); + names.addAll(grpcKeyBuilder.getNames()); + if (names.size() != prevSize + grpcKeyBuilder.getNames().size()) { + throw new IllegalStateException("Names in the GrpcKeyBuilders should be unique"); + } + } + } + + /** RequestProcessingStrategy specifies how to process a request when not already in the cache. */ + enum RequestProcessingStrategy { + /** + * Query the RLS and process the request using target returned by the lookup. The target will + * then be cached and used for processing subsequent requests for the same key. Any errors + * during lookup service processing will fall back to default target for request processing. + */ + SYNC_LOOKUP_DEFAULT_TARGET_ON_ERROR, + + /** + * Query the RLS and process the request using target returned by the lookup. The target will + * then be cached and used for processing subsequent requests for the same key. Any errors + * during lookup service processing will return an error back to the client. Services with + * strict regional routing requirements should use this strategy. + */ + SYNC_LOOKUP_CLIENT_SEES_ERROR, + + /** + * Query the RLS asynchronously but respond with the default target. The target in the lookup + * response will then be cached and used for subsequent requests. Services with strict latency + * requirements (but not strict regional routing requirements) should use this strategy. + */ + ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS; + } + + /** + * NameMatcher extract a key based on a given name (e.g. header name or query parameter name). + * The name must match one of the names listed in the "name" field. If the "required_match" field + * is true, one of the specified names must be present for the keybuilder to match. + */ + @Immutable + static final class NameMatcher { + + private final String key; + + private final ImmutableList names; + + private final boolean optional; + + NameMatcher(String key, List names, boolean optional) { + this.key = checkNotNull(key, "key"); + this.names = ImmutableList.copyOf(checkNotNull(names, "names")); + this.optional = optional; + } + + /** The name that will be used in the RLS key_map to refer to this value. */ + public String getKey() { + return key; + } + + /** Returns ordered list of names; the first non-empty value will be used. */ + public ImmutableList names() { + return names; + } + + /** + * Indicates if this extraction optional. A key builder will still match if no value is found. + */ + public boolean isOptional() { + return optional; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NameMatcher matcher = (NameMatcher) o; + return optional == matcher.optional + && java.util.Objects.equals(key, matcher.key) + && java.util.Objects.equals(names, matcher.names); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(key, names, optional); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("key", key) + .add("names", names) + .add("optional", optional) + .toString(); + } + } + + /** GrpcKeyBuilder is a configuration to construct headers consumed by route lookup service. */ + static final class GrpcKeyBuilder { + + private final ImmutableList names; + + private final ImmutableList headers; + + public GrpcKeyBuilder(List names, List headers) { + checkState(names != null && !names.isEmpty(), "names cannot be empty"); + this.names = ImmutableList.copyOf(names); + checkUniqueKey(checkNotNull(headers, "headers")); + this.headers = ImmutableList.copyOf(headers); + } + + private static void checkUniqueKey(List headers) { + Set names = new HashSet<>(); + for (NameMatcher header : headers) { + checkState(names.add(header.key), "key in headers must be unique"); + } + } + + /** + * Returns names. To match, one of the given Name fields must match; the service and method + * fields are specified as fixed strings. The service name is required and includes the proto + * package name. The method name may be omitted, in which case any method on the given service + * is matched. + */ + public ImmutableList getNames() { + return names; + } + + /** + * Returns a list of NameMatchers for header. Extract keys from all listed headers. For gRPC, it + * is an error to specify "required_match" on the NameMatcher protos, and we ignore it if set. + */ + public ImmutableList getHeaders() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GrpcKeyBuilder that = (GrpcKeyBuilder) o; + return Objects.equal(names, that.names) && Objects.equal(headers, that.headers); + } + + @Override + public int hashCode() { + return Objects.hashCode(names, headers); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("names", names) + .add("headers", headers) + .toString(); + } + + /** + * Name represents a method for a given service. To match, one of the given Name fields must + * match; the service and method fields are specified as fixed strings. The service name is + * required and includes the proto package name. The method name may be omitted, in which case + * any method on the given service is matched. + */ + static final class Name { + + private final String service; + + private final String method; + + public Name(String service) { + this(service, "*"); + } + + public Name(String service, String method) { + checkState( + !checkNotNull(service, "service").isEmpty(), + "service must not be empty or null"); + this.service = service; + this.method = method; + } + + public String getService() { + return service; + } + + public String getMethod() { + return method; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Name name = (Name) o; + return Objects.equal(service, name.service) + && Objects.equal(method, name.method); + } + + @Override + public int hashCode() { + return Objects.hashCode(service, method); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("service", service) + .add("method", method) + .toString(); + } + } + } +} diff --git a/rls/src/main/proto/grpc/lookup/v1/rls.proto b/rls/src/main/proto/grpc/lookup/v1/rls.proto new file mode 100644 index 0000000000..304a544dd6 --- /dev/null +++ b/rls/src/main/proto/grpc/lookup/v1/rls.proto @@ -0,0 +1,52 @@ +// Copyright 2020 The gRPC Authors +// +// 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. + +syntax = "proto3"; + +package grpc.lookup.v1; + +option go_package = "google.golang.org/grpc/lookup/grpc_lookup_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.lookup.v1"; +option java_outer_classname = "RlsProto"; + +message RouteLookupRequest { + // Full host name of the target server, e.g. firestore.googleapis.com. + // Only set for gRPC requests; HTTP requests must use key_map explicitly. + string server = 1; + // Full path of the request, i.e. "/service/method". + // Only set for gRPC requests; HTTP requests must use key_map explicitly. + string path = 2; + // Target type allows the client to specify what kind of target format it + // would like from RLS to allow it to find the regional server, e.g. "grpc". + string target_type = 3; + // Map of key values extracted via key builders for the gRPC or HTTP request. + map key_map = 4; +} + +message RouteLookupResponse { + // Actual addressable entity to use for routing decision, using syntax + // requested by the request target_type. + string target = 1; + // Optional header value to pass along to AFE in the X-Google-RLS-Data header. + // Cached with "target" and sent with all requests that match the request key. + // Allows the RLS to pass its work product to the eventual target. + string header_data = 2; +} + +service RouteLookupService { + // Lookup returns a target for a single key. + rpc RouteLookup(RouteLookupRequest) returns (RouteLookupResponse) {} +} + diff --git a/rls/src/main/proto/grpc/lookup/v1/rls_config.proto b/rls/src/main/proto/grpc/lookup/v1/rls_config.proto new file mode 100644 index 0000000000..6f941ae526 --- /dev/null +++ b/rls/src/main/proto/grpc/lookup/v1/rls_config.proto @@ -0,0 +1,215 @@ +// Copyright 2020 The gRPC Authors +// +// 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. + +syntax = "proto3"; + +package grpc.lookup.v1; + +import "google/protobuf/duration.proto"; + +option go_package = "google.golang.org/grpc/lookup/grpc_lookup_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.lookup.v1"; +option java_outer_classname = "RlsConfigProto"; + +// Extract a key based on a given name (e.g. header name or query parameter +// name). The name must match one of the names listed in the "name" field. If +// the "required_match" field is true, one of the specified names must be +// present for the keybuilder to match. +message NameMatcher { + // The name that will be used in the RLS key_map to refer to this value. + string key = 1; + + // Ordered list of names (headers or query parameter names) that can supply + // this value; the first one with a non-empty value is used. + repeated string names = 2; + + // If true, make this extraction required; the key builder will not match + // if no value is found. + bool required_match = 3; +} + +// A GrpcKeyBuilder applies to a given gRPC service, name, and headers. +message GrpcKeyBuilder { + // To match, one of the given Name fields must match; the service and method + // fields are specified as fixed strings. The service name is required and + // includes the proto package name. The method name may be omitted, in + // which case any method on the given service is matched. + message Name { + string service = 1; + string method = 2; + } + repeated Name names = 1; + + // Extract keys from all listed headers. + // For gRPC, it is an error to specify "required_match" on the NameMatcher + // protos, and we ignore it if set. + repeated NameMatcher headers = 2; +} + +// An HttpKeyBuilder applies to a given HTTP URL and headers. +// +// Path and host patterns use the matching syntax from gRPC transcoding to +// extract named key/value pairs from the path and host components of the URL: +// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto +// +// It is invalid to specify the same key name in multiple places in a pattern. +// +// For a service where the project id can be expressed either as a subdomain or +// in the path, separate HttpKeyBuilders must be used: +// host_pattern: 'example.com' path_pattern: '/{id}/{object}/**' +// host_pattern: '{id}.example.com' path_pattern: '/{object}/**' +// If the host is exactly 'example.com', the first path segment will be used as +// the id and the second segment as the object. If the host has a subdomain, the +// subdomain will be used as the id and the first segment as the object. If +// neither pattern matches, no keys will be extracted. +message HttpKeyBuilder { + // host_pattern is an ordered list of host template patterns for the desired + // value. If any host_pattern values are specified, then at least one must + // match, and the last one wins and sets any specified variables. A host + // consists of labels separated by dots. Each label is matched against the + // label in the pattern as follows: + // - "*": Matches any single label. + // - "**": Matches zero or more labels (first or last part of host only). + // - "{=...}": One or more label capture, where "..." can be any + // template that does not include a capture. + // - "{}": A single label capture. Identical to {=*}. + // + // Examples: + // - "example.com": Only applies to the exact host example.com. + // - "*.example.com": Matches subdomains of example.com. + // - "**.example.com": matches example.com, and all levels of subdomains. + // - "{project}.example.com": Extracts the third level subdomain. + // - "{project=**}.example.com": Extracts the third level+ subdomains. + // - "{project=**}": Extracts the entire host. + repeated string host_patterns = 1; + + // path_pattern is an ordered list of path template patterns for the desired + // value. If any path_pattern values are specified, then at least one must + // match, and the last one wins and sets any specified variables. A path + // consists of segments separated by slashes. Each segment is matched against + // the segment in the pattern as follows: + // - "*": Matches any single segment. + // - "**": Matches zero or more segments (first or last part of path only). + // - "{=...}": One or more segment capture, where "..." can be any + // template that does not include a capture. + // - "{}": A single segment capture. Identical to {=*}. + // A custom method may also be specified by appending ":" and the custom + // method name or "*" to indicate any custom method (including no custom + // method). For example, "/*/projects/{project_id}/**:*" extracts + // `{project_id}` for any version, resource and custom method that includes + // it. By default, any custom method will be matched. + // + // Examples: + // - "/v1/{name=messages/*}": extracts a name like "messages/12345". + // - "/v1/messages/{message_id}": extracts a message_id like "12345". + // - "/v1/users/{user_id}/messages/{message_id}": extracts two key values. + repeated string path_patterns = 2; + + // List of query parameter names to try to match. + // For example: ["parent", "name", "resource.name"] + // We extract all the specified query_parameters (case-sensitively). If any + // are marked as "required_match" and are not present, this keybuilder fails + // to match. If a given parameter appears multiple times (?foo=a&foo=b) we + // will report it as a comma-separated string (foo=a,b). + repeated NameMatcher query_parameters = 3; + + // List of headers to try to match. + // We extract all the specified header values (case-insensitively). If any + // are marked as "required_match" and are not present, this keybuilder fails + // to match. If a given header appears multiple times in the request we will + // report it as a comma-separated string, in standard HTTP fashion. + repeated NameMatcher headers = 4; +} + +message RouteLookupConfig { + // Ordered specifications for constructing keys for HTTP requests. Last + // match wins. If no HttpKeyBuilder matches, an empty key_map will be sent to + // the lookup service; it should likely reply with a global default route + // and raise an alert. + repeated HttpKeyBuilder http_keybuilders = 1; + + // Unordered specifications for constructing keys for gRPC requests. All + // GrpcKeyBuilders on this list must have unique "name" fields so that the + // client is free to prebuild a hash map keyed by name. If no GrpcKeyBuilder + // matches, an empty key_map will be sent to the lookup service; it should + // likely reply with a global default route and raise an alert. + repeated GrpcKeyBuilder grpc_keybuilders = 2; + + // The name of the lookup service as a gRPC URI. Typically, this will be + // a subdomain of the target, such as "lookup.datastore.googleapis.com". + string lookup_service = 3; + + // Configure a timeout value for lookup service requests. + // Defaults to 10 seconds if not specified. + google.protobuf.Duration lookup_service_timeout = 4; + + // How long are responses valid for (like HTTP Cache-Control). + // If omitted (i.e. 0), a default value of 5 minutes will be used. + // This value is clamped to 5 minutes to avoid unflushable bad responses. + google.protobuf.Duration max_age = 5; + + // After a response has been in the client cache for this amount of time + // and is re-requested, start an asynchronous RPC to re-validate it. + // This value should be less than max_age by at least the length of a + // typical RTT to the Route Lookup Service to fully mask the RTT latency. + // If omitted, keys are only re-requested after they have expired. + google.protobuf.Duration stale_age = 6; + + // Rough indicator of amount of memory to use for the client cache. Some of + // the data structure overhead is not accounted for, so actual memory consumed + // will be somewhat greater than this value. If this field is omitted or set + // to zero, a client default will be used. The value may be capped to a lower + // amount based on client configuration. + int64 cache_size_bytes = 7; + + // This is a list of all the possible targets that can be returned by the + // lookup service. If a target not on this list is returned, it will be + // treated the same as an RPC error from the RLS. + repeated string valid_targets = 8; + + // This value provides a default target to use if needed. It will be used for + // request processing strategy SYNC_LOOKUP_DEFAULT_TARGET_ON_ERROR if RLS + // returns an error, or strategy ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS if RLS + // returns an error or there is a cache miss in the client. It will also be + // used if there are no healthy backends for an RLS target. Note that + // requests can be routed only to a subdomain of the original target, + // e.g. "us_east_1.cloudbigtable.googleapis.com". + string default_target = 9; + + // Specify how to process a request when not already in the cache. + enum RequestProcessingStrategy { + STRATEGY_UNSPECIFIED = 0; + + // Query the RLS and process the request using target returned by the + // lookup. The target will then be cached and used for processing + // subsequent requests for the same key. Any errors during lookup service + // processing will fall back to default target for request processing. + SYNC_LOOKUP_DEFAULT_TARGET_ON_ERROR = 1; + + // Query the RLS and process the request using target returned by the + // lookup. The target will then be cached and used for processing + // subsequent requests for the same key. Any errors during lookup service + // processing will return an error back to the client. Services with + // strict regional routing requirements should use this strategy. + SYNC_LOOKUP_CLIENT_SEES_ERROR = 2; + + // Query the RLS asynchronously but respond with the default target. The + // target in the lookup response will then be cached and used for + // subsequent requests. Services with strict latency requirements (but not + // strict regional routing requirements) should use this strategy. + ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS = 3; + } + RequestProcessingStrategy request_processing_strategy = 10; +} diff --git a/rls/src/test/java/io/grpc/rls/internal/RlsProtoConvertersTest.java b/rls/src/test/java/io/grpc/rls/internal/RlsProtoConvertersTest.java new file mode 100644 index 0000000000..52cc851170 --- /dev/null +++ b/rls/src/test/java/io/grpc/rls/internal/RlsProtoConvertersTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc.rls.internal; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.base.Converter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.grpc.internal.JsonParser; +import io.grpc.lookup.v1.RouteLookupRequest; +import io.grpc.lookup.v1.RouteLookupResponse; +import io.grpc.rls.internal.RlsProtoConverters.RouteLookupConfigConverter; +import io.grpc.rls.internal.RlsProtoConverters.RouteLookupRequestConverter; +import io.grpc.rls.internal.RlsProtoConverters.RouteLookupResponseConverter; +import io.grpc.rls.internal.RlsProtoData.GrpcKeyBuilder; +import io.grpc.rls.internal.RlsProtoData.GrpcKeyBuilder.Name; +import io.grpc.rls.internal.RlsProtoData.NameMatcher; +import io.grpc.rls.internal.RlsProtoData.RequestProcessingStrategy; +import io.grpc.rls.internal.RlsProtoData.RouteLookupConfig; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class RlsProtoConvertersTest { + + @Test + public void convert_toRequestProto() { + Converter converter = + new RouteLookupRequestConverter(); + RouteLookupRequest proto = RouteLookupRequest.newBuilder() + .setServer("server") + .setPath("path") + .setTargetType("target") + .putKeyMap("key1", "val1") + .build(); + + RlsProtoData.RouteLookupRequest object = converter.convert(proto); + + assertThat(object.getServer()).isEqualTo("server"); + assertThat(object.getPath()).isEqualTo("path"); + assertThat(object.getTargetType()).isEqualTo("target"); + assertThat(object.getKeyMap()).containsExactly("key1", "val1"); + } + + @Test + public void convert_toRequestObject() { + Converter converter = + new RouteLookupRequestConverter().reverse(); + RlsProtoData.RouteLookupRequest requestObject = + new RlsProtoData.RouteLookupRequest( + "server", "path", "target", ImmutableMap.of("key1", "val1")); + + RouteLookupRequest proto = converter.convert(requestObject); + + assertThat(proto.getServer()).isEqualTo("server"); + assertThat(proto.getPath()).isEqualTo("path"); + assertThat(proto.getTargetType()).isEqualTo("target"); + assertThat(proto.getKeyMapMap()).containsExactly("key1", "val1"); + } + + @Test + public void convert_toResponseProto() { + Converter converter = + new RouteLookupResponseConverter(); + RouteLookupResponse proto = RouteLookupResponse.newBuilder() + .setTarget("target") + .setHeaderData("some header data") + .build(); + + RlsProtoData.RouteLookupResponse object = converter.convert(proto); + + assertThat(object.getTarget()).isEqualTo("target"); + assertThat(object.getHeaderData()).isEqualTo("some header data"); + } + + @Test + public void convert_toResponseObject() { + Converter converter = + new RouteLookupResponseConverter().reverse(); + + RlsProtoData.RouteLookupResponse object = + new RlsProtoData.RouteLookupResponse("target", "some header data"); + + RouteLookupResponse proto = converter.convert(object); + + assertThat(proto.getTarget()).isEqualTo("target"); + assertThat(proto.getHeaderData()).isEqualTo("some header data"); + } + + @Test + public void convert_jsonRlsConfig() throws IOException { + String jsonStr = "{\n" + + " \"grpcKeyBuilders\": [\n" + + " {\n" + + " \"names\": [\n" + + " {\n" + + " \"service\": \"service1\",\n" + + " \"method\": \"create\"\n" + + " }\n" + + " ],\n" + + " \"headers\": [\n" + + " {\n" + + " \"key\": \"user\"," + + " \"names\": [\"User\", \"Parent\"],\n" + + " \"optional\": true\n" + + " },\n" + + " {\n" + + " \"key\": \"id\"," + + " \"names\": [\"X-Google-Id\"],\n" + + " \"optional\": true\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"names\": [\n" + + " {\n" + + " \"service\": \"service1\",\n" + + " \"method\": \"*\"\n" + + " }\n" + + " ],\n" + + " \"headers\": [\n" + + " {\n" + + " \"key\": \"user\"," + + " \"names\": [\"User\", \"Parent\"],\n" + + " \"optional\": true\n" + + " },\n" + + " {\n" + + " \"key\": \"password\"," + + " \"names\": [\"Password\"],\n" + + " \"optional\": true\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"names\": [\n" + + " {\n" + + " \"service\": \"service3\",\n" + + " \"method\": \"*\"\n" + + " }\n" + + " ],\n" + + " \"headers\": [" + + " {\n" + + " \"key\": \"user\"," + + " \"names\": [\"User\", \"Parent\"],\n" + + " \"optional\": true\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"lookupService\": \"service1\",\n" + + " \"lookupServiceTimeout\": 2,\n" + + " \"maxAge\": 300,\n" + + " \"staleAge\": 240,\n" + + " \"validTargets\": [\"a valid target\"]," + + " \"cacheSizeBytes\": 1000,\n" + + " \"defaultTarget\": \"us_east_1.cloudbigtable.googleapis.com\",\n" + + " \"requestProcessingStrategy\": \"ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS\"\n" + + "}"; + + RouteLookupConfig expectedConfig = + new RouteLookupConfig( + ImmutableList.of( + new GrpcKeyBuilder( + ImmutableList.of(new Name("service1", "create")), + ImmutableList.of( + new NameMatcher("user", ImmutableList.of("User", "Parent"), true), + new NameMatcher("id", ImmutableList.of("X-Google-Id"), true))), + new GrpcKeyBuilder( + ImmutableList.of(new Name("service1")), + ImmutableList.of( + new NameMatcher("user", ImmutableList.of("User", "Parent"), true), + new NameMatcher("password", ImmutableList.of("Password"), true))), + new GrpcKeyBuilder( + ImmutableList.of(new Name("service3")), + ImmutableList.of( + new NameMatcher("user", ImmutableList.of("User", "Parent"), true)))), + /* lookupService= */ "service1", + /* lookupServiceTimeoutInMillis= */ TimeUnit.SECONDS.toMillis(2), + /* maxAgeInMillis= */ TimeUnit.SECONDS.toMillis(300), + /* staleAgeInMillis= */ TimeUnit.SECONDS.toMillis(240), + /* cacheSize= */ 1000, + /* validTargets= */ ImmutableList.of("a valid target"), + /* defaultTarget= */ "us_east_1.cloudbigtable.googleapis.com", + RequestProcessingStrategy.ASYNC_LOOKUP_DEFAULT_TARGET_ON_MISS); + + RouteLookupConfigConverter converter = new RouteLookupConfigConverter(); + @SuppressWarnings("unchecked") + Map parsedJson = (Map) JsonParser.parse(jsonStr); + RouteLookupConfig converted = converter.convert(parsedJson); + assertThat(converted).isEqualTo(expectedConfig); + } +} diff --git a/settings.gradle b/settings.gradle index 7881b4e80c..05a6da12d3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -47,6 +47,7 @@ include ":grpc-benchmarks" include ":grpc-services" include ":grpc-xds" include ":grpc-bom" +include ":grpc-rls" project(':grpc-api').projectDir = "$rootDir/api" as File project(':grpc-core').projectDir = "$rootDir/core" as File @@ -70,6 +71,7 @@ project(':grpc-benchmarks').projectDir = "$rootDir/benchmarks" as File project(':grpc-services').projectDir = "$rootDir/services" as File project(':grpc-xds').projectDir = "$rootDir/xds" as File project(':grpc-bom').projectDir = "$rootDir/bom" as File +project(':grpc-rls').projectDir = "$rootDir/rls" as File if (settings.hasProperty('skipCodegen') && skipCodegen.toBoolean()) { println '*** Skipping the build of codegen and compilation of proto files because skipCodegen=true'