diff --git a/docs/src/main/asciidoc/se/grpc/client.adoc b/docs/src/main/asciidoc/se/grpc/client.adoc index b60237daba4..ca5a957c4a2 100644 --- a/docs/src/main/asciidoc/se/grpc/client.adoc +++ b/docs/src/main/asciidoc/se/grpc/client.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2019, 2023 Oracle and/or its affiliates. + Copyright (c) 2019, 2024 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,10 +27,173 @@ include::{rootdir}/includes/se.adoc[] == Contents - <> +- <> +- <> +** <> +** <> +** <> +** <> +- <> == Overview -gRPC client is temporarily removed from Helidon, please follow issue -https://github.com/helidon-io/helidon/issues/5418 +The Helidon gRPC client API is part of the WebClient API, but with specific support to +invoke remote procedures and to register handlers for responses. All four types of gRPC +calls are supported: unary, bi-directional, client stream and server stream. A +Helidon gRPC client can be configured either using generated stubs (the most popular +option) or using manually crafted service descriptors. + +include::{rootdir}/includes/dependencies.adoc[] + +[source,xml] +---- + + io.helidon.webclient + helidon-webclient-grpc + +---- + +== Usage + +=== Generated Stubs + +A Helidon gRPC client can be configured from generated protobuf stubs. In what follows, +we shall use the following proto file and the corresponding stubs generated using +the `protoc` command: + +[source, proto] +---- +syntax = "proto3"; +option java_package = "my.package"; + +service StringService { + rpc Upper (StringMessage) returns (StringMessage) {} + rpc Split (StringMessage) returns (stream StringMessage) {} +} + +message StringMessage { + string text = 1; +} +---- + +The gRPC protocol runs on top of HTTP/2, and as such requires TLS configuration to +establish a connection. Thus, the first step is to configure TLS as shown next: + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_1, indent=0] +---- + +After creating a `Tls` instance, a `WebClient` can be created as follows: + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_2, indent=0] +---- + +So far, this is all the same as for accessing any protected REST endpoint; the +next step is to obtain a gRPC client stub using our newly created client. +This can be accomplished by _switching_ the client protocol to gRPC, and +using its channel to create a stub: + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_3, indent=0] +---- + +Once a stub is created, it can be used to invoke any of its declared +methods, such as `upper` to uppercase a string: + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_4, indent=0] +---- + +When it comes to invoking a method that can return more than one value, +there are two options: it can block (we are using virtual theads after all!) +and return back an `Iterator` or you can provide a `StreamObserver` as it +is more commonly done when using gRPC. Let's consider the case of the +`split` method that breaks up a sentence into individual words, and +can thus return multiple string messages. + +Using an iterator as a result: +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_5, indent=0] +---- + +Passing a stream observer and collecting all the messages into a `Future` +that returns an iterator: +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_6, indent=0] +---- + +=== Service Descriptors + +Service descriptors are an alternative to using generated stubs and the +`protoc` compiler. A service descriptor provides service meta-data to the +WebClient for the purpose of carrying out invocations. The descriptor +includes, the service name, and a description of each service method, +including its type, what it accepts and what it returns. + +The following is a descriptor for a service that includes the methods +called in the previous section using a stub: +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_7, indent=0] +---- + +Configuring a `WebClient` with `Tls` is done in the same manner as shown +above for the stub case. Once the gRPC client is created, a service +descriptor can be provided, and a method invoked using the methods +`unary`, `clientStream`, `serverStream` or `bidi`. For example, + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_8, indent=0] +---- + +=== Client URI Suppliers + +A `ClientURISupplier` can be used to dynamically obtain a sequence of `ClientUri` +instances to access when executing a gRPC request. If a client URI supplier is +configured, the Helidon gRPC implementation will attempt to connect to each +endpoint one by one, in the order provided, until a connection is successfully +established. This feature is useful in certain environments in which more than one +identical server is available, but with some potentially unavailable or unreachable. + +A few common implementations are provided in `ClientUriSuppliers`. These include +suppliers for strategies such as random, round-robin, among others. Applications +can either use one of the built-in suppliers or create their own. + +The following example configures a round-robin supplier using a collection +of known servers: + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_9, indent=0] +---- + +If both a base URI and a client URI supplier are configured, the latter will +take precendence over the former. + +=== gRPC Interceptors + +The gRPC API supports the notion of an interceptor on a channel. Interceptors are +useful to implement cross-cutting concerns that apply to many or all invocations. +These may include security, logging, metrics, etc. They can be specified directly +on the channel returned by a `GrpcClient`, effectively _wrapping_ that channel +with a list of interceptors to execute on every invocation. + +[source,java] +---- +include::{sourcedir}/se/grpc/ClientSnippets.java[tag=snippet_10, indent=0] +---- + +== Configuration + +TLS can be configured externally, just like it is done when using the +WebClient to access an HTTP endpoint. For more information see +https://helidon.io/docs/v4/se/webclient#_configuring_the_webclient[Configuring the WebClient]. -If you require gRPC client, either stay with a previous version of Helidon, or allow us to finish fixing the issue above. diff --git a/docs/src/main/java/io/helidon/docs/se/grpc/ClientSnippets.java b/docs/src/main/java/io/helidon/docs/se/grpc/ClientSnippets.java new file mode 100644 index 00000000000..5d90cc91102 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/grpc/ClientSnippets.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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.helidon.docs.se.grpc; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.config.Config; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.api.ClientUri; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.grpc.GrpcService; +import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; +import io.helidon.webclient.grpc.ClientUriSupplier; +import io.helidon.webclient.grpc.ClientUriSuppliers.RoundRobinSupplier; + +import com.google.protobuf.Descriptors; +import io.grpc.Channel; +import io.grpc.stub.StreamObserver; +import io.grpc.ClientInterceptor; + +import static io.helidon.grpc.core.ResponseHelper.complete; + +@SuppressWarnings("ALL") +class ClientSnippets { + + class StringServiceGrpc { + + class StringServiceBlockingStub { + Strings.StringMessage upper(Strings.StringMessage msg) { + return null; + } + Iterator split(Strings.StringMessage msg) { + return null; + } + void split(Strings.StringMessage msg, StreamObserver observer) { + } + } + + static StringServiceGrpc.StringServiceBlockingStub newBlockingStub(Channel c) { + return null; + } + } + + class Strings { + + class StringMessage { + String getText() { + return null; + } + } + } + + Strings.StringMessage newMessage(String s) { + return null; + } + + ClientUri[] myServers() { + return null; + } + + List myInterceptors() { + return null; + } + + void snippets() { + // tag::snippet_1[] + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + // end::snippet_1[] + + // tag::snippet_2[] + WebClient webClient = WebClient.builder() + .tls(clientTls) + .baseUri("https://localhost:8080") + .build(); + // end::snippet_2[] + + // tag::snippet_3[] + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = + StringServiceGrpc.newBlockingStub(grpcClient.channel()); + // end::snippet_3[] + + // tag::snippet_4[] + Strings.StringMessage msg1 = newMessage("hello"); + Strings.StringMessage res1 = service.upper(msg1); + String uppercased = res1.getText(); + // end::snippet_4[] + + // tag::snippet_5[] + Strings.StringMessage msg2 = newMessage("hello world"); + Iterator res2 = service.split(msg2); + while (res2.hasNext()) { + // ... + } + // end::snippet_5[] + + // tag::snippet_6[] + Strings.StringMessage msg3 = newMessage("hello world"); + CompletableFuture> future = new CompletableFuture<>(); + service.split(msg3, new StreamObserver() { + private final List value = new ArrayList<>(); + + @Override + public void onNext(Strings.StringMessage value) { + this.value.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value.iterator()); + } + }); + // end::snippet_6[] + + // tag::snippet_7[] + GrpcServiceDescriptor serviceDescriptor = GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Upper", + GrpcClientMethodDescriptor.unary("StringService", "Upper") + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) + .build()) + .putMethod("Split", + GrpcClientMethodDescriptor.serverStreaming("StringService", "Split") + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) + .build()) + .build(); + // end::snippet_7[] + + // tag::snippet_8[] + Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) + .unary("Upper", newMessage("hello")); + // end::snippet_8[] + + } + + void snippets2() { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + + // tag::snippet_9[] + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .clientUriSupplier(RoundRobinSupplier.create(myServers())) + .build(); + // end::snippet_9[] + + // tag::snippet_10[] + Channel newChannel = grpcClient.channel(myInterceptors()); + // end::snippet_10[] + } +}