From 652712da4e48efeb1c3a22a87165857b0a0b52d7 Mon Sep 17 00:00:00 2001 From: Will Vuong Date: Tue, 3 Sep 2024 22:57:59 -0400 Subject: [PATCH 1/2] gRPC reflection v1 support --- vertx-grpcio-server/pom.xml | 13 ++ .../vertx/grpcio/server/GrpcServerIndex.java | 170 ++++++++++++++++ .../server/ReflectionServiceV1Handler.java | 188 ++++++++++++++++++ .../src/main/proto/reflection.proto | 146 ++++++++++++++ .../tests/server/ReflectionServiceV1Test.java | 97 +++++++++ 5 files changed, 614 insertions(+) create mode 100644 vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/GrpcServerIndex.java create mode 100644 vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/ReflectionServiceV1Handler.java create mode 100644 vertx-grpcio-server/src/main/proto/reflection.proto create mode 100644 vertx-grpcio-server/src/test/java/io/vertx/tests/server/ReflectionServiceV1Test.java diff --git a/vertx-grpcio-server/pom.xml b/vertx-grpcio-server/pom.xml index cf9c6bfb..13997c99 100644 --- a/vertx-grpcio-server/pom.xml +++ b/vertx-grpcio-server/pom.xml @@ -106,6 +106,19 @@ + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + compile + + compile + compile-custom + + + + diff --git a/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/GrpcServerIndex.java b/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/GrpcServerIndex.java new file mode 100644 index 00000000..03088f40 --- /dev/null +++ b/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/GrpcServerIndex.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2011-2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.grpcio.server; + +import com.google.protobuf.Descriptors; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.protobuf.ProtoFileDescriptorSupplier; + +import java.util.*; +import java.util.function.Function; + +// Copied from https://github.com/quarkusio/quarkus/blob/main/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/GrpcServerIndex.java +public class GrpcServerIndex { + + private final Set names; + private final Map descriptorsByName; + private final Map descriptorsBySymbol; + private final Map> descriptorsByExtensionAndNumber; + + public GrpcServerIndex(List definitions) { + Queue fileDescriptorsToProcess = new ArrayDeque<>(); + Set files = new HashSet<>(); + Set names = new HashSet<>(); + Map descriptorsByName = new LinkedHashMap<>(); + Map descriptorsBySymbol = new LinkedHashMap<>(); + Map> descriptorsByExtensionAndNumber = new LinkedHashMap<>(); + + // Collect the services + for (ServerServiceDefinition definition : definitions) { + ServiceDescriptor serviceDescriptor = definition.getServiceDescriptor(); + if (serviceDescriptor.getSchemaDescriptor() instanceof ProtoFileDescriptorSupplier) { + ProtoFileDescriptorSupplier supplier = (ProtoFileDescriptorSupplier) serviceDescriptor + .getSchemaDescriptor(); + Descriptors.FileDescriptor fd = supplier.getFileDescriptor(); + String serviceName = serviceDescriptor.getName(); + if (names.contains(serviceName)) { + throw new IllegalStateException("Duplicated gRPC service: " + serviceName); + } + names.add(serviceName); + + if (!files.contains(fd.getName())) { + files.add(fd.getName()); + fileDescriptorsToProcess.add(fd); + } + } + } + + // Traverse the set of service and add dependencies + while (!fileDescriptorsToProcess.isEmpty()) { + Descriptors.FileDescriptor fd = fileDescriptorsToProcess.remove(); + processFileDescriptor(fd, descriptorsByName, descriptorsBySymbol, descriptorsByExtensionAndNumber); + for (Descriptors.FileDescriptor dep : fd.getDependencies()) { + if (!files.contains(dep.getName())) { + files.add(dep.getName()); + fileDescriptorsToProcess.add(dep); + } + } + } + + this.descriptorsByName = Collections.unmodifiableMap(descriptorsByName); + this.descriptorsByExtensionAndNumber = Collections.unmodifiableMap(descriptorsByExtensionAndNumber); + this.descriptorsBySymbol = Collections.unmodifiableMap(descriptorsBySymbol); + this.names = Collections.unmodifiableSet(names); + } + + public Set getServiceNames() { + return names; + } + + public Descriptors.FileDescriptor getFileDescriptorByName(String name) { + return descriptorsByName.get(name); + } + + public Descriptors.FileDescriptor getFileDescriptorBySymbol(String symbol) { + return descriptorsBySymbol.get(symbol); + } + + public Descriptors.FileDescriptor getFileDescriptorByExtensionAndNumber(String type, int number) { + Map map = descriptorsByExtensionAndNumber + .getOrDefault(type, Collections.emptyMap()); + return map.get(number); + } + + public Set getExtensionNumbersOfType(String type) { + return descriptorsByExtensionAndNumber.getOrDefault(type, Collections.emptyMap()).keySet(); + } + + private void processFileDescriptor(Descriptors.FileDescriptor fd, + Map descriptorsByName, + Map descriptorsBySymbol, + Map> descriptorsByExtensionAndNumber) { + String name = fd.getName(); + if (descriptorsByName.containsKey(name)) { + throw new IllegalStateException("File name already used: " + name); + } + descriptorsByName.put(name, fd); + for (Descriptors.ServiceDescriptor service : fd.getServices()) { + processService(service, fd, descriptorsBySymbol); + } + for (Descriptors.Descriptor type : fd.getMessageTypes()) { + processType(type, fd, descriptorsBySymbol, descriptorsByExtensionAndNumber); + } + for (Descriptors.FieldDescriptor extension : fd.getExtensions()) { + processExtension(extension, fd, descriptorsByExtensionAndNumber); + } + } + + private void processService(Descriptors.ServiceDescriptor service, Descriptors.FileDescriptor fd, + Map descriptorsBySymbol) { + String fullyQualifiedServiceName = service.getFullName(); + if (descriptorsBySymbol.containsKey(fullyQualifiedServiceName)) { + throw new IllegalStateException("Service already defined: " + fullyQualifiedServiceName); + } + descriptorsBySymbol.put(fullyQualifiedServiceName, fd); + for (Descriptors.MethodDescriptor method : service.getMethods()) { + String fullyQualifiedMethodName = method.getFullName(); + if (descriptorsBySymbol.containsKey(fullyQualifiedMethodName)) { + throw new IllegalStateException( + "Method already defined: " + fullyQualifiedMethodName + " in " + fullyQualifiedServiceName); + } + descriptorsBySymbol.put(fullyQualifiedMethodName, fd); + } + } + + private void processType(Descriptors.Descriptor type, Descriptors.FileDescriptor fd, + Map descriptorsBySymbol, + Map> descriptorsByExtensionAndNumber) { + String fullyQualifiedTypeName = type.getFullName(); + if (descriptorsBySymbol.containsKey(fullyQualifiedTypeName)) { + throw new IllegalStateException("Type already defined: " + fullyQualifiedTypeName); + } + descriptorsBySymbol.put(fullyQualifiedTypeName, fd); + for (Descriptors.FieldDescriptor extension : type.getExtensions()) { + processExtension(extension, fd, descriptorsByExtensionAndNumber); + } + for (Descriptors.Descriptor nestedType : type.getNestedTypes()) { + processType(nestedType, fd, descriptorsBySymbol, descriptorsByExtensionAndNumber); + } + } + + private void processExtension(Descriptors.FieldDescriptor extension, Descriptors.FileDescriptor fd, + Map> descriptorsByExtensionAndNumber) { + String extensionName = extension.getContainingType().getFullName(); + int extensionNumber = extension.getNumber(); + + descriptorsByExtensionAndNumber.computeIfAbsent(extensionName, + new Function>() { + @Override + public Map apply(String s) { + return new HashMap<>(); + } + }); + + if (descriptorsByExtensionAndNumber.get(extensionName).containsKey(extensionNumber)) { + throw new IllegalStateException( + "Extension name " + extensionName + " and number " + extensionNumber + " are already defined"); + } + descriptorsByExtensionAndNumber.get(extensionName).put(extensionNumber, fd); + } + +} diff --git a/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/ReflectionServiceV1Handler.java b/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/ReflectionServiceV1Handler.java new file mode 100644 index 00000000..f3f07603 --- /dev/null +++ b/vertx-grpcio-server/src/main/java/io/vertx/grpcio/server/ReflectionServiceV1Handler.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2011-2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.grpcio.server; + +import com.google.protobuf.Descriptors; +import io.grpc.Status; +import io.grpc.reflection.v1.*; +import io.vertx.core.Handler; +import io.vertx.grpc.server.GrpcServerRequest; +import io.vertx.grpc.server.GrpcServerResponse; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Queue; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +// Copied from https://github.com/quarkusio/quarkus/blob/main/extensions/grpc/reflection/src/main/java/io/quarkus/grpc/reflection/service/ReflectionServiceV1.java +// And adapted for Vertx +public class ReflectionServiceV1Handler implements Handler> { + + private final GrpcServerIndex index; + + public ReflectionServiceV1Handler(GrpcServerIndex index) { + this.index = index; + } + + @Override + public void handle(GrpcServerRequest request) { + request.handler(serverReflectionRequest -> { + GrpcServerResponse response = request.response(); + switch (serverReflectionRequest.getMessageRequestCase()) { + case LIST_SERVICES: + response.end(getServiceList(serverReflectionRequest)); + break; + case FILE_BY_FILENAME: + response.end(getFileByName(serverReflectionRequest)); + break; + case FILE_CONTAINING_SYMBOL: + response.end(getFileContainingSymbol(serverReflectionRequest)); + break; + case FILE_CONTAINING_EXTENSION: + response.end(getFileByExtension(serverReflectionRequest)); + break; + case ALL_EXTENSION_NUMBERS_OF_TYPE: + response.end(getAllExtensions(serverReflectionRequest)); + break; + default: + response.end(getErrorResponse(serverReflectionRequest, Status.Code.UNIMPLEMENTED, + "not implemented " + serverReflectionRequest.getMessageRequestCase())); + } + }); + } + + private ServerReflectionResponse getServiceList(ServerReflectionRequest request) { + ListServiceResponse response = index.getServiceNames().stream() + .map(new Function() { // NOSONAR + @Override + public ServiceResponse apply(String s) { + return ServiceResponse.newBuilder().setName(s).build(); + } + }) + .collect(new Supplier() { + @Override + public ListServiceResponse.Builder get() { + return ListServiceResponse.newBuilder(); + } + }, + new BiConsumer() { + @Override + public void accept(ListServiceResponse.Builder builder, ServiceResponse value) { + builder.addService(value); + } + }, + new BiConsumer() { // NOSONAR + @Override + public void accept(ListServiceResponse.Builder b1, + ListServiceResponse.Builder b2) { + b1.addAllService(b2.getServiceList()); + } + }) + .build(); + + return ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setListServicesResponse(response) + .build(); + } + + private ServerReflectionResponse getFileByName(ServerReflectionRequest request) { + String name = request.getFileByFilename(); + Descriptors.FileDescriptor fd = index.getFileDescriptorByName(name); + if (fd != null) { + return getServerReflectionResponse(request, fd); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, "File not found (" + name + ")"); + } + } + + private ServerReflectionResponse getFileContainingSymbol(ServerReflectionRequest request) { + String symbol = request.getFileContainingSymbol(); + Descriptors.FileDescriptor fd = index.getFileDescriptorBySymbol(symbol); + if (fd != null) { + return getServerReflectionResponse(request, fd); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, "Symbol not found (" + symbol + ")"); + } + } + + private ServerReflectionResponse getFileByExtension(ServerReflectionRequest request) { + ExtensionRequest extensionRequest = request.getFileContainingExtension(); + String type = extensionRequest.getContainingType(); + int extension = extensionRequest.getExtensionNumber(); + Descriptors.FileDescriptor fd = index.getFileDescriptorByExtensionAndNumber(type, extension); + if (fd != null) { + return getServerReflectionResponse(request, fd); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, + "Extension not found (" + type + ", " + extension + ")"); + } + } + + private ServerReflectionResponse getAllExtensions(ServerReflectionRequest request) { + String type = request.getAllExtensionNumbersOfType(); + Set extensions = index.getExtensionNumbersOfType(type); + if (extensions != null) { + ExtensionNumberResponse.Builder builder = ExtensionNumberResponse.newBuilder() + .setBaseTypeName(type) + .addAllExtensionNumber(extensions); + return ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setAllExtensionNumbersResponse(builder) + .build(); + } else { + return getErrorResponse(request, Status.Code.NOT_FOUND, "Type not found."); + } + } + + private ServerReflectionResponse getServerReflectionResponse( + ServerReflectionRequest request, Descriptors.FileDescriptor fd) { + FileDescriptorResponse.Builder fdRBuilder = FileDescriptorResponse.newBuilder(); + + // Traverse the descriptors to get the full list of dependencies and add them to the builder + Set seenFiles = new HashSet<>(); + Queue frontier = new ArrayDeque<>(); + seenFiles.add(fd.getName()); + frontier.add(fd); + while (!frontier.isEmpty()) { + Descriptors.FileDescriptor nextFd = frontier.remove(); + fdRBuilder.addFileDescriptorProto(nextFd.toProto().toByteString()); + for (Descriptors.FileDescriptor dependencyFd : nextFd.getDependencies()) { + if (!seenFiles.contains(dependencyFd.getName())) { + seenFiles.add(dependencyFd.getName()); + frontier.add(dependencyFd); + } + } + } + return ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setFileDescriptorResponse(fdRBuilder) + .build(); + } + + private ServerReflectionResponse getErrorResponse( + ServerReflectionRequest request, Status.Code code, String message) { + return ServerReflectionResponse.newBuilder() + .setValidHost(request.getHost()) + .setOriginalRequest(request) + .setErrorResponse( + ErrorResponse.newBuilder() + .setErrorCode(code.value()) + .setErrorMessage(message)) + .build(); + } +} diff --git a/vertx-grpcio-server/src/main/proto/reflection.proto b/vertx-grpcio-server/src/main/proto/reflection.proto new file mode 100644 index 00000000..f9f349fe --- /dev/null +++ b/vertx-grpcio-server/src/main/proto/reflection.proto @@ -0,0 +1,146 @@ +// Copyright 2016 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + +syntax = "proto3"; + +package grpc.reflection.v1; + +option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.reflection.v1"; +option java_outer_classname = "ServerReflectionProto"; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server sets one of the following fields according to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. + // As the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requests. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services requests. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} diff --git a/vertx-grpcio-server/src/test/java/io/vertx/tests/server/ReflectionServiceV1Test.java b/vertx-grpcio-server/src/test/java/io/vertx/tests/server/ReflectionServiceV1Test.java new file mode 100644 index 00000000..ca7e50e4 --- /dev/null +++ b/vertx-grpcio-server/src/test/java/io/vertx/tests/server/ReflectionServiceV1Test.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2011-2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.tests.server; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.reflection.v1.ListServiceResponse; +import io.grpc.reflection.v1.ServerReflectionGrpc; +import io.grpc.reflection.v1.ServerReflectionRequest; +import io.grpc.reflection.v1.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.grpcio.server.GrpcIoServer; +import io.vertx.grpcio.server.GrpcIoServiceBridge; +import io.vertx.grpcio.server.GrpcServerIndex; +import io.vertx.grpcio.server.ReflectionServiceV1Handler; +import org.junit.Test; + +import java.util.List; + +public class ReflectionServiceV1Test extends ServerTestBase { + + @Test + public void testReflection(TestContext should) { + // server stub + GreeterGrpc.GreeterImplBase impl = new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + responseObserver.onNext(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); + responseObserver.onCompleted(); + } + }; + + // create grpc server handler + GrpcIoServer grpcServer = GrpcIoServer.server(vertx); + + // bind server stub + GrpcIoServiceBridge bridge = GrpcIoServiceBridge.bridge(impl); + bridge.bind(grpcServer); + + // create grpc proto index used by reflection service + GrpcServerIndex index = new GrpcServerIndex(List.of(impl.bindService())); + + // register proto reflection service handler + grpcServer.callHandler(ServerReflectionGrpc.getServerReflectionInfoMethod(), + new ReflectionServiceV1Handler(index)); + + // start server + startServer(grpcServer); + + // set up client stub + channel = ManagedChannelBuilder.forAddress("localhost", port) + .usePlaintext() + .build(); + + ServerReflectionGrpc.ServerReflectionStub stub = ServerReflectionGrpc.newStub(channel); + + // set up response observer + Async test = should.async(); + StreamObserver streamObserver = stub.serverReflectionInfo(new StreamObserver<>() { + @Override + public void onNext(ServerReflectionResponse serverReflectionResponse) { + ListServiceResponse listServicesResponse = serverReflectionResponse.getListServicesResponse(); + should.assertEquals(1, listServicesResponse.getServiceCount()); + should.assertEquals("helloworld.Greeter", listServicesResponse.getService(0).getName()); + } + + @Override + public void onError(Throwable throwable) { + should.fail(throwable); + } + + @Override + public void onCompleted() { + test.complete(); + } + }); + + // send request + ServerReflectionRequest request = ServerReflectionRequest.newBuilder().setListServices("").build(); + streamObserver.onNext(request); + + // wait for test completion + test.await(); + } +} From 958a184a031fdd406a185580235c199def664821 Mon Sep 17 00:00:00 2001 From: Will Vuong Date: Thu, 5 Sep 2024 21:08:25 -0400 Subject: [PATCH 2/2] Add docs --- .../src/main/asciidoc/ioserver.adoc | 20 +++++++++++ .../java/examples/GrpcIoServerExamples.java | 34 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/vertx-grpc-docs/src/main/asciidoc/ioserver.adoc b/vertx-grpc-docs/src/main/asciidoc/ioserver.adoc index 8c1d95ea..927af9a2 100644 --- a/vertx-grpc-docs/src/main/asciidoc/ioserver.adoc +++ b/vertx-grpc-docs/src/main/asciidoc/ioserver.adoc @@ -38,3 +38,23 @@ The Vert.x gRPC Server can bridge a gRPC service to use with _grpc-java_ generat ---- The bridge supports deadline automatic cancellation: when a gRPC request carrying a timeout is received, a deadline is associated with the `io.grpc.Context` an can be obtained from the current context. This deadline automatically cancels the request in progress when its associated timeout fires. + +=== gRPC Reflection APIs + +Support for the https://grpc.io/docs/guides/reflection/[gRPC reflection APIs] can be added to your Vert.x gRPC Server. + +[source,java] +---- +{@link examples.GrpcIoServerExamples#reflectionExample} +---- + +You can then use tools like https://github.com/fullstorydev/grpcurl[gRPCurl] to explore and invoke your gRPC APIs. + +[source,bash] +---- +grpcurl -plaintext localhost:50051 list + +grpcurl -plaintext localhost:50051 describe .helloworld.HelloRequest + +grpcurl -plaintext -d '{"name": "Vert.x"}' localhost:50051 helloworld.Greeter +---- diff --git a/vertx-grpc-docs/src/main/java/examples/GrpcIoServerExamples.java b/vertx-grpc-docs/src/main/java/examples/GrpcIoServerExamples.java index fa9e5702..3186bb7b 100644 --- a/vertx-grpc-docs/src/main/java/examples/GrpcIoServerExamples.java +++ b/vertx-grpc-docs/src/main/java/examples/GrpcIoServerExamples.java @@ -1,5 +1,6 @@ package examples; +import io.grpc.reflection.v1.ServerReflectionGrpc; import io.grpc.stub.StreamObserver; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; @@ -7,6 +8,10 @@ import io.vertx.docgen.Source; import io.vertx.grpcio.server.GrpcIoServer; import io.vertx.grpcio.server.GrpcIoServiceBridge; +import io.vertx.grpcio.server.GrpcServerIndex; +import io.vertx.grpcio.server.ReflectionServiceV1Handler; + +import java.util.List; @Source public class GrpcIoServerExamples { @@ -43,4 +48,33 @@ public void sayHello(HelloRequest request, StreamObserver responseOb .requestHandler(grpcServer) .listen(); } + + public void reflectionExample(Vertx vertx, HttpServerOptions options) { + + GrpcIoServer grpcServer = GrpcIoServer.server(vertx); + + GreeterGrpc.GreeterImplBase service = new GreeterGrpc.GreeterImplBase() { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + responseObserver.onNext(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); + responseObserver.onCompleted(); + } + }; + + // Bind the service bridge in the gRPC server + GrpcIoServiceBridge serverStub = GrpcIoServiceBridge.bridge(service); + serverStub.bind(grpcServer); + + // Create grpc proto index used by reflection service + GrpcServerIndex index = new GrpcServerIndex(List.of(service.bindService())); + + // Register proto reflection service handler + grpcServer.callHandler(ServerReflectionGrpc.getServerReflectionInfoMethod(), + new ReflectionServiceV1Handler(index)); + + // Start the HTTP/2 server + vertx.createHttpServer(options) + .requestHandler(grpcServer) + .listen(); + } }