From 19827f53d3a1eaa140cb7e189ff82bc6c518b51f Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Jul 2024 10:05:24 -0400 Subject: [PATCH 01/12] Implements the gRPC MP Client API. This API uses interfaces to create client proxies. It introduces two new annotations, GrpcProxy and GrpcChannel (also defined as part of the Grpc namespace). Client proxies can be injected into CDI beans to access remote gRPC services. --- .../main/java/io/helidon/grpc/api/Grpc.java | 23 + .../io/helidon/grpc/core/package-info.java | 2 +- microprofile/grpc/client/pom.xml | 161 +++++ .../grpc/client/ChannelProducer.java | 97 +++ .../grpc/client/ClientMethodDescriptor.java | 414 +++++++++++ .../microprofile/grpc/client/ClientProxy.java | 70 ++ .../grpc/client/ClientRequestAttribute.java | 58 ++ .../grpc/client/ClientServiceDescriptor.java | 655 ++++++++++++++++++ .../grpc/client/ClientTracingInterceptor.java | 409 +++++++++++ .../grpc/client/DelegatingBeanAttributes.java | 98 +++ .../GrpcChannelDescriptorBlueprint.java | 62 ++ .../grpc/client/GrpcChannelsProvider.java | 209 ++++++ .../grpc/client/GrpcClientBuilder.java | 195 ++++++ .../grpc/client/GrpcClientCdiExtension.java | 107 +++ .../grpc/client/GrpcProxyBuilder.java | 79 +++ .../grpc/client/GrpcProxyProducer.java | 69 ++ .../grpc/client/GrpcServiceClient.java | 471 +++++++++++++ .../grpc/client/package-info.java | 20 + .../client/src/main/java/module-info.java | 41 ++ .../client/ClientMethodDescriptorTest.java | 139 ++++ .../client/ClientServiceDescriptorTest.java | 292 ++++++++ .../grpc/client/EchoServiceTest.java | 139 ++++ .../grpc/client/JavaMarshaller.java | 71 ++ .../src/test/java/services/EchoService.java | 53 ++ .../test/java/services/TreeMapService.java | 271 ++++++++ .../grpc/client/src/test/proto/echo.proto | 30 + .../grpc/client/src/test/proto/strings.proto | 31 + .../src/test/resources/application.yaml | 47 ++ .../grpc/client/src/test/resources/client.p12 | Bin 0 -> 4181 bytes .../src/test/resources/logging.properties | 33 + .../grpc/client/src/test/resources/server.p12 | Bin 0 -> 4133 bytes microprofile/grpc/pom.xml | 1 + 32 files changed, 4346 insertions(+), 1 deletion(-) create mode 100644 microprofile/grpc/client/pom.xml create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java create mode 100644 microprofile/grpc/client/src/main/java/module-info.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java create mode 100644 microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java create mode 100644 microprofile/grpc/client/src/test/java/services/EchoService.java create mode 100644 microprofile/grpc/client/src/test/java/services/TreeMapService.java create mode 100644 microprofile/grpc/client/src/test/proto/echo.proto create mode 100644 microprofile/grpc/client/src/test/proto/strings.proto create mode 100644 microprofile/grpc/client/src/test/resources/application.yaml create mode 100644 microprofile/grpc/client/src/test/resources/client.p12 create mode 100644 microprofile/grpc/client/src/test/resources/logging.properties create mode 100644 microprofile/grpc/client/src/test/resources/server.p12 diff --git a/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java b/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java index 4b763f4ec22..2bbfd89f802 100644 --- a/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java +++ b/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java @@ -293,4 +293,27 @@ public interface Grpc { */ Class value(); } + + /** + * An annotation that can be used to specify the name of a configured gRPC channel. + */ + @Target({TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PARAMETER}) + @Retention(RUNTIME) + @interface GrpcChannel { + + /** + * The name of the configured channel. + * + * @return name of the channel + */ + String value(); + } + + /** + * An annotation used to mark an injection point for a gRPC service client proxy. + */ + @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) + @Retention(RUNTIME) + @interface GrpcProxy { + } } diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java index a10d014bfed..4c5124c1aa9 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java @@ -15,6 +15,6 @@ */ /** - * Core classes used by both the gRPC server API and gRPC client API. + * Core classes used by both the gRPC server and gRPC client. */ package io.helidon.grpc.core; diff --git a/microprofile/grpc/client/pom.xml b/microprofile/grpc/client/pom.xml new file mode 100644 index 00000000000..e6980fae5e2 --- /dev/null +++ b/microprofile/grpc/client/pom.xml @@ -0,0 +1,161 @@ + + + + + 4.0.0 + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-project + 4.1.0-SNAPSHOT + + + helidon-microprofile-grpc-client + Helidon Microprofile gRPC Client + + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.grpc + helidon-grpc-api + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-core + + + io.helidon.config + helidon-config + + + io.helidon.tracing + helidon-tracing + + + io.helidon.webclient + helidon-webclient-grpc + + + io.helidon.common + helidon-common-tls + + + io.helidon.codegen + helidon-codegen + + + jakarta.inject + jakarta.inject-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + io.helidon.webserver + helidon-webserver-grpc + test + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-server + test + + + io.helidon.config + helidon-config-yaml + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + javax.annotation + javax.annotation-api + provided + true + + + org.mockito + mockito-core + test + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + test-compile + test-compile-custom + + + + + + + \ No newline at end of file diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java new file mode 100644 index 00000000000..bcfc6d675ac --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java @@ -0,0 +1,97 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import io.helidon.config.Config; +import io.helidon.grpc.api.Grpc; + +import io.grpc.Channel; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; + +/** + * A producer of gRPC {@link io.grpc.Channel Channels}. + */ +@ApplicationScoped +public class ChannelProducer { + + private final GrpcChannelsProvider provider; + private final ReentrantLock lock = new ReentrantLock(); + private final Map channelMap = new HashMap<>(); + + /** + * Create a {@link ChannelProducer}. + * + * @param config the {@link io.helidon.config.Config} to use to configure + * the provided {@link io.grpc.Channel}s + */ + @Inject + ChannelProducer(Config config) { + provider = GrpcChannelsProvider.create(config.get("grpc")); + } + + /** + * Produces a gRPC {@link io.grpc.Channel}. + * + * @param injectionPoint the injection point + * @return a gRPC {@link io.grpc.Channel} + */ + @Produces + @Grpc.GrpcChannel(value = GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) + public Channel get(InjectionPoint injectionPoint) { + Grpc.GrpcChannel qualifier = injectionPoint.getQualifiers() + .stream() + .filter(q -> q.annotationType().equals(Grpc.GrpcChannel.class)) + .map(q -> (Grpc.GrpcChannel) q) + .findFirst() + .orElse(null); + + String name = (qualifier == null) ? GrpcChannelsProvider.DEFAULT_CHANNEL_NAME : qualifier.value(); + return findChannel(name); + } + + /** + * Produces the default gRPC {@link io.grpc.Channel}. + * + * @return the default gRPC {@link io.grpc.Channel} + */ + @Produces + public Channel getDefaultChannel() { + return findChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME); + } + + /** + * Obtain the named {@link io.grpc.Channel}. + * + * @param name the channel name + * @return the named {@link io.grpc.Channel} + */ + public Channel findChannel(String name) { + try { + lock.lock(); + return channelMap.computeIfAbsent(name, provider::channel); + } finally { + lock.unlock(); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java new file mode 100644 index 00000000000..01a1260ac71 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptor.java @@ -0,0 +1,414 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.util.Arrays; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.grpc.core.WeightedBag; + +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; + +/** + * Encapsulates all metadata necessary to define a gRPC method. In addition to wrapping a {@link MethodDescriptor}, + * this class also holds the request and response types of the gRPC method. A + * {@link ClientServiceDescriptor} can contain zero or more {@link MethodDescriptor}. + *

+ * An instance of ClientMethodDescriptor can be created either from an existing {@link MethodDescriptor} or + * from one of the factory methods {@link #bidirectional(String, String)}, {@link #clientStreaming(String, String)}, + * {@link #serverStreaming(String, String)} or {@link #unary(String, String)}. + */ +public final class ClientMethodDescriptor { + + /** + * The simple name of the method. + */ + private final String name; + + /** + * The {@link MethodDescriptor} for this method. This is usually obtained from protocol buffer + * method getDescriptor (from service getDescriptor). + */ + private final MethodDescriptor descriptor; + + /** + * The list of client interceptors for this method. + */ + private final WeightedBag interceptors; + + /** + * The {@link CallCredentials} for this method. + */ + private final CallCredentials callCredentials; + + /** + * The method handler for this method. + */ + private final MethodHandler methodHandler; + + private ClientMethodDescriptor(String name, + MethodDescriptor descriptor, + WeightedBag interceptors, + CallCredentials callCredentials, + MethodHandler methodHandler) { + this.name = name; + this.descriptor = descriptor; + this.interceptors = interceptors; + this.callCredentials = callCredentials; + this.methodHandler = methodHandler; + } + + /** + * Creates a new {@link Builder} with the specified name and {@link MethodDescriptor}. + * + * @param serviceName the name of the owning gRPC service + * @param name the simple method name + * @param descriptor the underlying gRPC {@link MethodDescriptor.Builder} + * @return A new instance of a {@link Builder} + */ + static Builder builder(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { + return new Builder(serviceName, name, descriptor); + } + + /** + * Creates a new {@link Builder} with the specified + * name and {@link MethodDescriptor}. + * + * @param serviceName the name of the owning gRPC service + * @param name the simple method name + * @param descriptor the underlying gRPC {@link MethodDescriptor.Builder} + * @return a new instance of a {@link Builder} + */ + static ClientMethodDescriptor create(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { + return builder(serviceName, name, descriptor).build(); + } + + /** + * Creates a new unary {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder unary(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.UNARY); + } + + /** + * Creates a new client Streaming {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder clientStreaming(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.CLIENT_STREAMING); + } + + /** + * Creates a new server streaming {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder serverStreaming(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.SERVER_STREAMING); + } + + /** + * Creates a new bidirectional {@link Builder} with + * the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @return a new instance of a {@link Builder} + */ + static Builder bidirectional(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.BIDI_STREAMING); + } + + /** + * Return the {@link CallCredentials} set on this service. + * + * @return the {@link CallCredentials} set on this service + */ + public CallCredentials callCredentials() { + return this.callCredentials; + } + + /** + * Creates a new {@link Builder} with the specified name. + * + * @param serviceName the name of the owning gRPC service + * @param name the method name + * @param methodType the gRPC method type + * @return a new instance of a {@link Builder} + */ + static Builder builder(String serviceName, + String name, + MethodDescriptor.MethodType methodType) { + + MethodDescriptor.Builder builder = MethodDescriptor.newBuilder() + .setFullMethodName(serviceName + "/" + name) + .setType(methodType); + + return new Builder(serviceName, name, builder) + .requestType(Object.class) + .responseType(Object.class); + } + + /** + * Returns the simple name of the method. + * + * @return The simple name of the method. + */ + public String name() { + return name; + } + + /** + * Returns the {@link MethodDescriptor} of this method. + * + * @param the request type + * @param the response type + * @return The {@link MethodDescriptor} of this method. + */ + @SuppressWarnings("unchecked") + public MethodDescriptor descriptor() { + return (MethodDescriptor) descriptor; + } + + /** + * Obtain the {@link ClientInterceptor}s to use for this method. + * + * @return the {@link ClientInterceptor}s to use for this method + */ + WeightedBag interceptors() { + return interceptors.readOnly(); + } + + /** + * Obtain the {@link MethodHandler} to use to make client calls. + * + * @return the {@link MethodHandler} to use to make client calls + */ + public MethodHandler methodHandler() { + return methodHandler; + } + + /** + * ClientMethod configuration API. + */ + public interface Rules { + + /** + * Sets the type of parameter of this method. + * + * @param type The type of parameter of this method. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules requestType(Class type); + + /** + * Sets the type of parameter of this method. + * + * @param type The type of parameter of this method. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules responseType(Class type); + + /** + * Register one or more {@link ClientInterceptor interceptors} for the method. + * + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + */ + Rules intercept(ClientInterceptor... interceptors); + + /** + * Register one or more {@link ClientInterceptor interceptors} for the method. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more {@link ClientInterceptor}s to register + * @return this {@link Rules} to allow fluent method chaining + */ + Rules intercept(int priority, ClientInterceptor... interceptors); + + /** + * Register the {@link MarshallerSupplier} for the method. + *

+ * If not set the default {@link MarshallerSupplier} from the service will be used. + * + * @param marshallerSupplier the {@link MarshallerSupplier} for the service + * @return this {@link Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Register the specified {@link CallCredentials} to be used for this method. This overrides + * any {@link CallCredentials} set on the {@link ClientServiceDescriptor}. + * + * @param callCredentials the {@link CallCredentials} to set. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules callCredentials(CallCredentials callCredentials); + + /** + * Set the {@link MethodHandler} that can be used to invoke the method. + * + * @param methodHandler the {@link MethodHandler} to use + * @return this {@link Rules} instance for fluent call chaining + */ + Rules methodHandler(MethodHandler methodHandler); + } + + /** + * {@link MethodDescriptor} builder implementation. + */ + public static class Builder + implements Rules, io.helidon.common.Builder { + + private String name; + private final MethodDescriptor.Builder descriptor; + private Class requestType; + private Class responseType; + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.create(); + private MarshallerSupplier marshallerSupplier; + private CallCredentials callCredentials; + private MethodHandler methodHandler; + + /** + * Constructs a new Builder instance. + * + * @param serviceName The name of the service ths method belongs to + * @param name the name of this method + * @param descriptor The gRPC method descriptor builder + */ + Builder(String serviceName, String name, MethodDescriptor.Builder descriptor) { + this.name = name; + this.descriptor = descriptor.setFullMethodName(serviceName + "/" + name); + } + + @Override + public Builder requestType(Class type) { + this.requestType = type; + return this; + } + + @Override + public Builder responseType(Class type) { + this.responseType = type; + return this; + } + + @Override + public Builder intercept(ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Builder intercept(int priority, ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + @Override + public Builder marshallerSupplier(MarshallerSupplier supplier) { + this.marshallerSupplier = supplier; + return this; + } + + Builder defaultMarshallerSupplier(MarshallerSupplier supplier) { + if (supplier == null) { + this.defaultMarshallerSupplier = MarshallerSupplier.create(); + } else { + this.defaultMarshallerSupplier = supplier; + } + return this; + } + + @Override + public Builder methodHandler(MethodHandler methodHandler) { + this.methodHandler = methodHandler; + return this; + } + + /** + * Sets the full name of this Method. + * + * @param fullName the full name of the method + * @return this builder instance for fluent API + */ + Builder fullName(String fullName) { + descriptor.setFullMethodName(fullName); + this.name = fullName.substring(fullName.lastIndexOf('/') + 1); + return this; + } + + @Override + public Rules callCredentials(CallCredentials callCredentials) { + this.callCredentials = callCredentials; + return this; + } + + /** + * Builds and returns a new instance of {@link ClientMethodDescriptor}. + * + * @return a new instance of {@link ClientMethodDescriptor} + */ + @Override + @SuppressWarnings("unchecked") + public ClientMethodDescriptor build() { + MarshallerSupplier supplier = this.marshallerSupplier; + + if (supplier == null) { + supplier = defaultMarshallerSupplier; + } + + if (requestType != null) { + descriptor.setRequestMarshaller((MethodDescriptor.Marshaller) supplier.get(requestType)); + } + + if (responseType != null) { + descriptor.setResponseMarshaller((MethodDescriptor.Marshaller) supplier.get(responseType)); + } + + return new ClientMethodDescriptor(name, + descriptor.build(), + interceptors, + callCredentials, + methodHandler); + } + + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java new file mode 100644 index 00000000000..18883921d95 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientProxy.java @@ -0,0 +1,70 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * A dynamic proxy that forwards methods to gRPC call handlers. + */ +class ClientProxy implements InvocationHandler { + + private final GrpcServiceClient client; + + /** + * A map of Java method name to gRPR method name. + */ + private final Map names; + + /** + * Create a {@link ClientProxy}. + * + * @param client the {@link GrpcServiceClient} to use + * @param names a map of Java method names to gRPC method names + */ + private ClientProxy(GrpcServiceClient client, Map names) { + this.client = client; + this.names = names; + } + + /** + * Create a {@link ClientProxy} instance. + * + * @param client the {@link GrpcServiceClient} to use + * @param names a map of Java method names to gRPC method names + * @return a {@link ClientProxy} instance for the specified service client + */ + static ClientProxy create(GrpcServiceClient client, Map names) { + return new ClientProxy(client, names); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + return client.invoke(names.get(method.getName()), args); + } + + /** + * Obtain the underlying {@link GrpcServiceClient}. + * + * @return the underlying {@link GrpcServiceClient} + */ + public GrpcServiceClient getClient() { + return client; + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java new file mode 100644 index 00000000000..4e47dfd6b34 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientRequestAttribute.java @@ -0,0 +1,58 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +/** + * An enum of possible gRPC client call attributes to attach to + * call tracing spans. + */ +public enum ClientRequestAttribute { + /** + * Add the method type to the tracing span. + */ + METHOD_TYPE, + + /** + * Add the method name to the tracing span. + */ + METHOD_NAME, + + /** + * Add the call deadline to the tracing span. + */ + DEADLINE, + + /** + * Add the compressor type to the tracing span. + */ + COMPRESSOR, + + /** + * Add the security authority to the tracing span. + */ + AUTHORITY, + + /** + * Add the method call options to the tracing span. + */ + ALL_CALL_OPTIONS, + + /** + * Add the method call headers to the tracing span. + */ + HEADERS +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java new file mode 100644 index 00000000000..f31ed4a9754 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptor.java @@ -0,0 +1,655 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.WeightedBag; + +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors; +import io.grpc.BindableService; +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.ServiceDescriptor; + +import static io.helidon.grpc.core.GrpcHelper.extractMethodName; + +/** + * Encapsulates all details about a client side gRPC service. + */ +public class ClientServiceDescriptor { + + private final String serviceName; + private final Map methods; + private final WeightedBag interceptors; + private final CallCredentials callCredentials; + + private ClientServiceDescriptor(String serviceName, + Map methods, + WeightedBag interceptors, + CallCredentials callCredentials) { + this.serviceName = serviceName; + this.methods = methods; + this.interceptors = interceptors; + this.callCredentials = callCredentials; + } + + /** + * Create a {@link ClientServiceDescriptor} from a {@link ServiceDescriptor}. + * + * @param descriptor the {@link ServiceDescriptor} + * @return a {@link ClientServiceDescriptor} + */ + public static ClientServiceDescriptor create(ServiceDescriptor descriptor) { + return builder(descriptor).build(); + } + + /** + * Create a {@link ClientServiceDescriptor} from a {@link BindableService}. + * + * @param service the BindableService + * @return a {@link ClientServiceDescriptor} + */ + public static ClientServiceDescriptor create(BindableService service) { + return builder(service).build(); + } + + /** + * Create a {@link Builder} from a {@link ServiceDescriptor}. + * + * @param service the {@link ServiceDescriptor} + * @return a {@link Builder} + */ + public static Builder builder(ServiceDescriptor service) { + return new Builder(service); + } + + /** + * Create a {@link Builder} from a {@link BindableService}. + * + * @param service the {@link BindableService} + * @return a {@link Builder} + */ + public static Builder builder(BindableService service) { + return new Builder(service); + } + + /** + * Create a {@link Builder} form a name and type. + *

+ * The {@link Class#getSimpleName() class simple name} will be used for the service name. + * + * @param serviceClass the service class + * @return a {@link Builder} + */ + public static Builder builder(Class serviceClass) { + try { + Method method = serviceClass.getMethod("getServiceDescriptor"); + if (method.getReturnType() == ServiceDescriptor.class) { + ServiceDescriptor svcDesc = (ServiceDescriptor) method.invoke(null); + return builder(svcDesc); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException itEx) { + // Ignored. + } + return builder(serviceClass.getSimpleName(), serviceClass); + } + + /** + * Create a {@link Builder} form a name and type. + * + * @param serviceName the getName of the service to use to initialise the builder + * @param serviceClass the service class + * @return a {@link Builder} + */ + public static Builder builder(String serviceName, Class serviceClass) { + return new Builder(serviceName, serviceClass); + } + + /** + * Obtain the service name. + * + * @return the service name + */ + public String name() { + return serviceName; + } + + /** + * Return {@link ClientMethodDescriptor} for a specified method getName. + * + * @param name method getName + * @return method getDescriptor for the specified getName + */ + public ClientMethodDescriptor method(String name) { + return methods.get(name); + } + + /** + * Return the collections of methods that make up this service. + * + * @return service methods + */ + public Collection methods() { + return Collections.unmodifiableCollection(methods.values()); + } + + /** + * Return service interceptors. + * + * @return service interceptors + */ + public WeightedBag interceptors() { + return interceptors.readOnly(); + } + + /** + * Return the {@link CallCredentials} set on this service. + * + * @return the {@link CallCredentials} set on this service + */ + public CallCredentials callCredentials() { + return this.callCredentials; + } + + @Override + public String toString() { + return "ClientServiceDescriptor(name='" + serviceName + "')"; + } + + // ---- inner interface: Rules ----------------------------------------- + + /** + * Fluent configuration interface for the {@link ClientServiceDescriptor}. + */ + public interface Rules { + /** + * Obtain the name fo the service this configuration configures. + * + * @return the name fo the service this configuration configures + */ + String name(); + + /** + * Set the name for the service. + * + * @param name the name of service + * @return this {@link Rules} instance for fluent call chaining + * @throws NullPointerException if the getName is null + * @throws IllegalArgumentException if the getName is a blank String + */ + Rules name(String name); + + /** + * Register the proto file for the service. + * + * @param proto the service proto + * @return this {@link Rules} instance for fluent call chaining + */ + Rules proto(Descriptors.FileDescriptor proto); + + /** + * Register the {@link MarshallerSupplier} for the service. + * + * @param marshallerSupplier the {@link MarshallerSupplier} for the service + * @return this {@link Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Register one or more {@link ClientInterceptor interceptors} for the service. + * + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + */ + Rules intercept(ClientInterceptor... interceptors); + + /** + * Add one or more {@link ClientInterceptor} instances that will intercept calls + * to this service. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param priority the priority to assign to the interceptors + * @param interceptors one or more {@link ClientInterceptor}s to add + * @return this builder to allow fluent method chaining + */ + Rules intercept(int priority, ClientInterceptor... interceptors); + + /** + * Register one or more {@link ClientInterceptor interceptors} for a named method of the service. + * + * @param methodName the name of the method to intercept + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + * @throws IllegalArgumentException if no method exists for the specified getName + */ + Rules intercept(String methodName, ClientInterceptor... interceptors); + + /** + * Register one or more {@link ClientInterceptor interceptors} for a named method of the service. + *

+ * The added interceptors will be applied using the specified priority. + * + * @param methodName the name of the method to intercept + * @param priority the priority to assign to the interceptors + * @param interceptors the interceptor(s) to register + * @return this {@link Rules} instance for fluent call chaining + * + * @throws IllegalArgumentException if no method exists for the specified name + */ + Rules intercept(String methodName, int priority, ClientInterceptor... interceptors); + + /** + * Register unary method for the service. + * + * @param name The getName of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules unary(String name); + + /** + * Register unary method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules unary(String name, Consumer configurer); + + /** + * Register server streaming method for the service. + * + * @param name The name of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name); + + /** + * Register server streaming method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules serverStreaming(String name, Consumer configurer); + + /** + * Register client streaming method for the service. + * + * @param name The name of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name); + + /** + * Register client streaming method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules clientStreaming(String name, Consumer configurer); + + /** + * Register bi-directional streaming method for the service. + * + * @param name The name of the method + * @return this {@link Rules} instance for fluent call chaining + */ + Rules bidirectional(String name); + + /** + * Register bi-directional streaming method for the service. + * + * @param name the name of the method + * @param configurer the method configurer + * @return this {@link Rules} instance for fluent call chaining + */ + Rules bidirectional(String name, Consumer configurer); + + /** + * Register the {@link CallCredentials} to be used for this service. + * + * @param callCredentials the {@link CallCredentials} to set. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules callCredentials(CallCredentials callCredentials); + + /** + * Register the {@link CallCredentials} to be used for the specified method in this service. This overrides + * any {@link CallCredentials} set on this {@link ClientServiceDescriptor} + * + * @param name the method name + * @param callCredentials the {@link CallCredentials} to set. + * @return this {@link Rules} instance for fluent call chaining + */ + Rules callCredentials(String name, CallCredentials callCredentials); + + } + + // ---- inner class: BaseBuilder -------------------------------------------- + + /** + * A {@link ClientServiceDescriptor} builder. + */ + public static final class Builder implements Rules, io.helidon.common.Builder { + + private String name; + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private final Class serviceClass; + private Descriptors.FileDescriptor proto; + private MarshallerSupplier marshallerSupplier = MarshallerSupplier.create(); + private CallCredentials callCredentials; + + private final Map methodBuilders = new HashMap<>(); + + /** + * Builds the ClientService from a {@link BindableService}. + * + * @param service the {@link BindableService} to use to initialize the builder + */ + private Builder(BindableService service) { + this(service.bindService().getServiceDescriptor()); + } + + /** + * Builds the ClientService from a {@link BindableService}. + * + * @param serviceDescriptor the {@link ServiceDescriptor} to use to initialize the builder + */ + private Builder(ServiceDescriptor serviceDescriptor) { + this.name = serviceDescriptor.getName(); + this.serviceClass = serviceDescriptor.getClass(); + + for (io.grpc.MethodDescriptor md : serviceDescriptor.getMethods()) { + String methodName = extractMethodName(md.getFullMethodName()); + + methodBuilders.put(methodName, ClientMethodDescriptor.builder(this.name, methodName, md.toBuilder())); + } + } + + /** + * Create a new {@link Builder}. + * + * @param name the service name + * @param serviceClass the service class + */ + private Builder(String name, Class serviceClass) { + this.name = name; + this.serviceClass = serviceClass; + } + + @Override + public String name() { + return name; + } + + @Override + public Builder name(String serviceName) { + if (serviceName == null) { + throw new NullPointerException("Service getName cannot be null"); + } + + if (serviceName.trim().isEmpty()) { + throw new IllegalArgumentException("Service getName cannot be blank"); + } + + this.name = serviceName.trim(); + for (Map.Entry e : methodBuilders.entrySet()) { + e.getValue().fullName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, e.getKey())); + } + return this; + } + + @Override + public Builder proto(Descriptors.FileDescriptor proto) { + this.proto = proto; + return this; + } + + @Override + public Builder marshallerSupplier(MarshallerSupplier marshallerSupplier) { + this.marshallerSupplier = marshallerSupplier; + return this; + } + + @Override + public Builder unary(String name) { + return unary(name, null); + } + + @Override + public Builder unary(String name, Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.UNARY, configurer)); + return this; + } + + @Override + public Builder serverStreaming(String name) { + return serverStreaming(name, null); + } + + @Override + public Builder serverStreaming(String name, + Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.SERVER_STREAMING, configurer)); + return this; + } + + @Override + public Builder clientStreaming(String name) { + return clientStreaming(name, null); + } + + @Override + public Builder clientStreaming(String name, + Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.CLIENT_STREAMING, configurer)); + return this; + } + + @Override + public Builder bidirectional(String name) { + return bidirectional(name, null); + } + + @Override + public Builder bidirectional(String name, + Consumer configurer) { + methodBuilders.put(name, createMethodDescriptor(name, MethodType.BIDI_STREAMING, configurer)); + return this; + } + + @Override + public Builder intercept(ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Rules intercept(int priority, ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), priority); + return this; + } + + @Override + public Builder intercept(String methodName, ClientInterceptor... interceptors) { + ClientMethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with getName '" + methodName + "'"); + } + + method.intercept(interceptors); + + return this; + } + + @Override + public Builder intercept(String methodName, int priority, ClientInterceptor... interceptors) { + ClientMethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with getName '" + methodName + "'"); + } + + method.intercept(priority, interceptors); + + return this; + } + + @Override + public Builder callCredentials(CallCredentials callCredentials) { + this.callCredentials = callCredentials; + return this; + } + + @Override + public Builder callCredentials(String methodName, CallCredentials callCredentials) { + ClientMethodDescriptor.Builder method = methodBuilders.get(methodName); + + if (method == null) { + throw new IllegalArgumentException("No method exists with getName '" + methodName + "'"); + } + + method.callCredentials(callCredentials); + return this; + } + + @Override + public ClientServiceDescriptor build() { + Map methods = new LinkedHashMap<>(); + for (Map.Entry entry : methodBuilders.entrySet()) { + methods.put(entry.getKey(), entry.getValue().build()); + } + + return new ClientServiceDescriptor(name, methods, interceptors, callCredentials); + } + + // ---- helpers ----------------------------------------------------- + + private ClientMethodDescriptor.Builder createMethodDescriptor( + String methodName, + MethodType methodType, + Consumer configurer) { + + io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() + .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(this.name, methodName)) + .setType(methodType) + .setSampledToLocalTracing(true); + + Class requestType = getTypeFromMethodDescriptor(methodName, true); + Class responseType = getTypeFromMethodDescriptor(methodName, false); + + ClientMethodDescriptor.Builder builder = ClientMethodDescriptor.builder(this.name, methodName, grpcDesc) + .defaultMarshallerSupplier(this.marshallerSupplier) + .requestType(requestType) + .responseType(responseType); + + if (configurer != null) { + configurer.accept(builder); + } + + return builder; + } + + private Class getTypeFromMethodDescriptor(String methodName, boolean fInput) { + + // if the proto is not present, assume that we are not using + // protobuf for marshalling and that whichever marshaller is used + // doesn't need type information (basically, that the serialized + // stream is self-describing) + if (proto == null) { + return Object.class; + } + + // todo: add error handling here, and fail fast with a more + // todo: meaningful exception (and message) than a NPE + // todo: if the service or the method cannot be found + Descriptors.ServiceDescriptor svc = proto.findServiceByName(name); + Descriptors.MethodDescriptor mtd = svc.findMethodByName(methodName); + Descriptors.Descriptor type = fInput ? mtd.getInputType() : mtd.getOutputType(); + + String pkg = getPackageName(); + String outerClass = getOuterClassName(); + + // make sure that any nested protobuf class names are converted + // into a proper Java binary class getName + String className = pkg + "." + outerClass + type.getFullName().replace('.', '$'); + + // the assumption here is that the protobuf generated classes can always + // be loaded by the same class loader that loaded the service class, + // as the service implementation is bound to depend on them + try { + return serviceClass != null + ? serviceClass.getClassLoader().loadClass(className) + : this.getClass().getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String getPackageName() { + String pkg = proto.getOptions().getJavaPackage(); + return "".equals(pkg) ? proto.getPackage() : pkg; + } + + private String getOuterClassName() { + DescriptorProtos.FileOptions options = proto.getOptions(); + if (options.getJavaMultipleFiles()) { + // there is no outer class -- each message will have its own top-level class + return ""; + } + + String outerClass = options.getJavaOuterClassname(); + if ("".equals(outerClass)) { + outerClass = getOuterClassFromFileName(proto.getName()); + } + + // append $ in order to timed a proper binary getName for the nested message class + return outerClass + "$"; + } + + private String getOuterClassFromFileName(String name) { + // strip .proto extension + name = name.substring(0, name.lastIndexOf(".proto")); + + String[] words = name.split("_"); + StringBuilder sb = new StringBuilder(name.length()); + + for (String word : words) { + sb.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)); + } + + return sb.toString(); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java new file mode 100644 index 00000000000..8a2278fe5b4 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java @@ -0,0 +1,409 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.helidon.common.Weight; +import io.helidon.grpc.core.ContextKeys; +import io.helidon.grpc.core.GrpcTracingContext; +import io.helidon.grpc.core.GrpcTracingName; +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.tracing.HeaderConsumer; +import io.helidon.tracing.HeaderProvider; +import io.helidon.tracing.Span; +import io.helidon.tracing.Tracer; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; + +/** + * A {@link ClientInterceptor} that captures tracing information into + * Tracing {@link Span}s for client calls. + */ +@Weight(InterceptorWeights.TRACING) +public class ClientTracingInterceptor implements ClientInterceptor { + + private final Tracer tracer; + + private final GrpcTracingName operationNameConstructor; + + private final boolean streaming; + + private final boolean verbose; + + private final Set tracedAttributes; + + /** + * Private constructor called by {@link Builder}. + * + * @param tracer the Open Tracing {@link Tracer} + * @param operationNameConstructor the operation name constructor + * @param streaming flag indicating whether to trace streaming calls + * @param verbose flag to indicate verbose logging to spans + * @param tracedAttributes the set of request attributes to add to the span + */ + private ClientTracingInterceptor(Tracer tracer, + GrpcTracingName operationNameConstructor, + boolean streaming, + boolean verbose, + Set tracedAttributes) { + this.tracer = tracer; + this.operationNameConstructor = operationNameConstructor; + this.streaming = streaming; + this.verbose = verbose; + this.tracedAttributes = tracedAttributes; + } + + /** + * Obtain a builder to build a {@link ClientTracingInterceptor}. + * + * @param tracer the {@link Tracer} to use + * @return a builder to build a {@link ClientTracingInterceptor} + */ + public static Builder builder(Tracer tracer) { + return new Builder(tracer); + } + + /** + * Use this interceptor to trace all requests made by this client channel. + * + * @param channel to be traced + * @return intercepted channel + */ + public Channel intercept(Channel channel) { + return ClientInterceptors.intercept(channel, this); + } + + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, + Channel next) { + + String operationName = operationNameConstructor.name(method); + Span span = createSpanFromParent(operationName); + + for (ClientRequestAttribute attr : tracedAttributes) { + switch (attr) { + case ALL_CALL_OPTIONS: + span.tag("grpc.call_options", callOptions.toString()); + break; + case AUTHORITY: + if (callOptions.getAuthority() == null) { + span.tag("grpc.authority", "null"); + } else { + span.tag("grpc.authority", callOptions.getAuthority()); + } + break; + case COMPRESSOR: + if (callOptions.getCompressor() == null) { + span.tag("grpc.compressor", "null"); + } else { + span.tag("grpc.compressor", callOptions.getCompressor()); + } + break; + case DEADLINE: + if (callOptions.getDeadline() == null) { + span.tag("grpc.deadline_millis", "null"); + } else { + span.tag("grpc.deadline_millis", callOptions.getDeadline().timeRemaining(TimeUnit.MILLISECONDS)); + } + break; + case METHOD_NAME: + span.tag("grpc.method_name", method.getFullMethodName()); + break; + case METHOD_TYPE: + if (method.getType() == null) { + span.tag("grpc.method_type", "null"); + } else { + span.tag("grpc.method_type", method.getType().toString()); + } + break; + case HEADERS: + break; + default: + // should not happen, but can be ignored + } + } + + return new ClientTracingListener<>(next.newCall(method, callOptions), span); + } + + private Span createSpanFromParent(String operationName) { + return tracer.spanBuilder(operationName) + .update(it -> GrpcTracingContext.activeSpan().map(Span::context).ifPresent(it::parent)) + .start(); + } + + /** + * Builds the configuration of a ClientTracingInterceptor. + */ + public static class Builder { + + private final Tracer tracer; + + private GrpcTracingName operationNameConstructor; + + private boolean streaming; + + private boolean verbose; + + private Set tracedAttributes; + + /** + * Constructs a builder from a {@link Tracer}. + * + * @param tracer to use for this interceptor + * Creates a Builder with default configuration + */ + public Builder(Tracer tracer) { + this.tracer = tracer; + operationNameConstructor = MethodDescriptor::getFullMethodName; + streaming = false; + verbose = false; + tracedAttributes = new HashSet<>(); + } + + /** + * Sets the operation name. + * + * @param operationNameConstructor to name all spans created by this interceptor + * @return this Builder with configured operation name + */ + public Builder withOperationName(GrpcTracingName operationNameConstructor) { + this.operationNameConstructor = operationNameConstructor; + return this; + } + + /** + * Logs streaming events to client spans. + * + * @return this Builder configured to log streaming events + */ + public Builder withStreaming() { + streaming = true; + return this; + } + + /** + * Sets one or more attributes. + * + * @param tracedAttributes to set as tags on client spans + * created by this interceptor + * @return this Builder configured to trace attributes + */ + public Builder withTracedAttributes(ClientRequestAttribute... tracedAttributes) { + this.tracedAttributes = new HashSet<>(Arrays.asList(tracedAttributes)); + return this; + } + + /** + * Logs all request life-cycle events to client spans. + * + * @return this Builder configured to be verbose + */ + public Builder withVerbosity() { + verbose = true; + return this; + } + + /** + * Finishes building the interceptor. + * + * @return a ClientTracingInterceptor with this Builder's configuration + */ + public ClientTracingInterceptor build() { + return new ClientTracingInterceptor(tracer, + operationNameConstructor, + streaming, + verbose, + tracedAttributes); + } + } + + /** + * A {@link SimpleForwardingClientCall} that adds information + * to a tracing {@link Span} at different places in the gROC call lifecycle. + * + * @param the gRPC request type + * @param the gRPC response type + */ + private class ClientTracingListener + extends ForwardingClientCall.SimpleForwardingClientCall { + + private final Span span; + + private ClientTracingListener(ClientCall delegate, Span span) { + super(delegate); + this.span = span; + } + + @Override + public void start(Listener responseListener, final Metadata headers) { + if (verbose) { + span.addEvent("Started call"); + } + + if (tracedAttributes.contains(ClientRequestAttribute.HEADERS)) { + // copy the headers and make sure that the AUTHORIZATION header + // is removed as we do not want auth details to appear in tracing logs + Metadata metadata = new Metadata(); + metadata.merge(headers); + metadata.removeAll(ContextKeys.AUTHORIZATION); + span.tag("grpc.headers", metadata.toString()); + } + + tracer.inject(span.context(), HeaderProvider.empty(), new MetadataHeaderConsumer(headers)); + + Listener tracingResponseListener + = new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + if (verbose) { + span.addEvent("headers", + Map.of("Response headers received", headers.toString())); + } + delegate().onHeaders(headers); + } + + @Override + public void onClose(Status status, Metadata trailers) { + if (verbose) { + if (status.getCode().value() == 0) { + span.addEvent("Call closed"); + } else { + String desc = String.valueOf(status.getDescription()); + + span.addEvent("onClose", Map.of("Call failed", desc)); + } + } + span.end(); + + delegate().onClose(status, trailers); + } + + @Override + public void onMessage(RespT message) { + if (streaming || verbose) { + span.addEvent("Response received"); + } + delegate().onMessage(message); + } + }; + + delegate().start(tracingResponseListener, headers); + } + + @Override + public void sendMessage(ReqT message) { + if (streaming || verbose) { + span.addEvent("Message sent"); + } + + delegate().sendMessage(message); + } + + @Override + public void cancel(String message, Throwable cause) { + String errorMessage; + + errorMessage = message == null ? "Error" : message; + + if (cause == null) { + span.addEvent(errorMessage); + } else { + span.addEvent("error", Map.of(errorMessage, cause.getMessage())); + } + + delegate().cancel(message, cause); + } + + @Override + public void halfClose() { + if (streaming) { + span.addEvent("Finished sending messages"); + } + + delegate().halfClose(); + } + + private class MetadataHeaderConsumer implements HeaderConsumer { + private final Metadata headers; + + private MetadataHeaderConsumer(Metadata headers) { + this.headers = headers; + } + + @Override + public void setIfAbsent(String key, String... values) { + Metadata.Key headerKey = key(key); + if (!headers.containsKey(headerKey)) { + headers.put(headerKey, values[0]); + } + } + + @Override + public void set(String key, String... values) { + Metadata.Key headerKey = key(key); + headers.put(headerKey, values[0]); + } + + @Override + public Iterable keys() { + return headers.keys(); + } + + @Override + public Optional get(String key) { + return Optional.ofNullable(headers.get(key(key))); + } + + @Override + public Iterable getAll(String key) { + // map single value to list or get an empty list + return get(key).map(List::of).orElseGet(List::of); + } + + @Override + public boolean contains(String key) { + return headers.containsKey(key(key)); + } + + private Metadata.Key key(String name) { + return Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER); + } + + private void put(Metadata.Key key, String value) { + headers.put(key, value); + } + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java new file mode 100644 index 00000000000..04333d9fd81 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/DelegatingBeanAttributes.java @@ -0,0 +1,98 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +import jakarta.enterprise.inject.spi.BeanAttributes; + +/** + * A {@link jakarta.enterprise.inject.spi.BeanAttributes} implementation. + * + * @param the class of the bean instance + */ +class DelegatingBeanAttributes implements BeanAttributes { + + private final BeanAttributes delegate; + private final Set types; + + /** + * Create a {@link DelegatingBeanAttributes}. + * + * @param delegate the {@link jakarta.enterprise.inject.spi.BeanAttributes} to delegate to + * @param types the {@link java.lang.reflect.Type}s for this bean + */ + private DelegatingBeanAttributes(BeanAttributes delegate, Set types) { + super(); + Objects.requireNonNull(delegate); + this.delegate = delegate; + this.types = Collections.unmodifiableSet(types); + } + + /** + * Create a {@link DelegatingBeanAttributes}. + * + * @param delegate the {@link jakarta.enterprise.inject.spi.BeanAttributes} to delegate to + * @param types the {@link java.lang.reflect.Type}s for this bean + */ + static DelegatingBeanAttributes create(BeanAttributes delegate, Set types) { + return new DelegatingBeanAttributes<>(delegate, types); + } + + @Override + public String getName() { + return this.delegate.getName(); + } + + @Override + public Set getQualifiers() { + return this.delegate.getQualifiers(); + } + + @Override + public Class getScope() { + return this.delegate.getScope(); + } + + @Override + public Set> getStereotypes() { + return this.delegate.getStereotypes(); + } + + @Override + public Set getTypes() { + if (types == null || types.isEmpty()) { + return this.delegate.getTypes(); + } else { + return types; + } + } + + @Override + public boolean isAlternative() { + return this.delegate.isAlternative(); + } + + @Override + public String toString() { + return this.delegate.toString(); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java new file mode 100644 index 00000000000..74d77fcadf1 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java @@ -0,0 +1,62 @@ +/* + * 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.microprofile.grpc.client; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.tls.Tls; + +@Prototype.Blueprint +@Prototype.Configured +interface GrpcChannelDescriptorBlueprint { + + /** + * The host to connect to. + * + * @return the host + */ + @Option.Configured + @Option.Default(GrpcChannelsProvider.DEFAULT_HOST) + String host(); + + /** + * The port to connect to. + * + * @return the port + */ + @Option.Configured + @Option.DefaultInt(GrpcChannelsProvider.DEFAULT_PORT) + int port(); + + /** + * The target URI. + * + * @return the URI + */ + @Option.Configured + Optional target(); + + /** + * TLS configuration for the connection. + * + * @return the TLS config + */ + @Option.Configured + Optional tls(); +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java new file mode 100644 index 00000000000..db566648d0f --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java @@ -0,0 +1,209 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import io.helidon.common.tls.Tls; +import io.helidon.config.Config; +import io.helidon.webclient.grpc.GrpcClient; + +import io.grpc.Channel; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.enterprise.inject.spi.Extension; + +/** + * GrpcChannelsProvider is a factory for pre-configured gRPC Channel instances. + */ +public class GrpcChannelsProvider { + + /** + * A constant for holding the default channel configuration name (which is "default"). + */ + public static final String DEFAULT_CHANNEL_NAME = "default"; + + /** + * A constant for holding the default host name (which is "localhost"). + */ + public static final String DEFAULT_HOST = "localhost"; + + /** + * The configuration key for the channels' configuration. + */ + public static final String CFG_KEY_CHANNELS = "channels"; + + /** + * A constant for holding the default port (which is "1408"). + */ + public static final int DEFAULT_PORT = 1408; + + private final Map channelConfigs; + + private GrpcChannelsProvider(Map channelDescriptors) { + this.channelConfigs = new HashMap<>(channelDescriptors); + } + + /** + * Builds a new instance of {@link GrpcChannelsProvider} using default configuration. The + * default configuration connects to "localhost:1408" without TLS. + * + * @return a new instance of {@link GrpcChannelsProvider} + */ + public static GrpcChannelsProvider create() { + return GrpcChannelsProvider.builder().build(); + } + + /** + * Creates a {@link GrpcChannelsProvider} using the specified configuration. + * + * @param config The externalized configuration. + * @return a new instance of {@link GrpcChannelsProvider} + */ + public static GrpcChannelsProvider create(Config config) { + return new Builder(config).build(); + } + + /** + * Create a new {@link Builder}. + * + * @return a new {@link Builder} + */ + public static Builder builder() { + return builder(null); + } + + /** + * Create a new {@link Builder}. + * + * @param config the {@link Config} to bootstrap from + * @return a new {@link Builder} + */ + public static Builder builder(Config config) { + return new Builder(config); + } + + // --------------- private methods of GrpcChannelsProvider --------------- + + /** + * Returns a {@link Channel} for the specified channel or host name. + *

+ * If the specified channel name does not exist in the configuration, we will assume + * that it represents the name of the gRPC host to connect to and will create a plain text + * channel to the host with the specified {@code name}, on a default port (1408). + * + * @param name the name of the channel configuration as specified in the configuration file, + * or the name of the host to connect to + * @return a new instance of {@link Channel} + * @throws NullPointerException if name is null + * @throws IllegalArgumentException if name is empty + */ + public Channel channel(String name) { + if (name == null) { + throw new NullPointerException("name cannot be null."); + } + if (name.trim().isEmpty()) { + throw new IllegalArgumentException("name cannot be empty or blank."); + } + GrpcChannelDescriptor chCfg = channelConfigs.computeIfAbsent(name, hostName -> + GrpcChannelDescriptor.builder().host(name).build()); + return createChannel(name, chCfg); + } + + Channel createChannel(String name, GrpcChannelDescriptor descriptor) { + Tls clientTls = descriptor.tls().orElse(null); + if (clientTls == null) { + throw new IllegalArgumentException("Client TLS must be configured for gRPC proxy client"); + } + int port = descriptor.port(); + if (port <= 0) { + port = discoverServerPort(); + } + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://" + descriptor.host() + ":" + port) + .build(); + return grpcClient.channel(); + } + + /** + * Used for unit testing: if port not set, then obtain port from CDI extension + * via reflection. Should be moved to a testing module. + * + * @return server port + */ + @SuppressWarnings("unchecked") + private static int discoverServerPort() { + try { + Class extClass = (Class) Class + .forName("io.helidon.microprofile.server.ServerCdiExtension"); + Extension extension = CDI.current().getBeanManager().getExtension(extClass); + Method m = extension.getClass().getMethod("port", String.class); + return (int) m.invoke(extension, new Object[] {"@default"}); + } catch (ReflectiveOperationException e) { + return 0; + } + } + + /** + * Builder builds an instance of {@link GrpcChannelsProvider}. + */ + public static class Builder implements io.helidon.common.Builder { + + private final Map channelConfigs = new HashMap<>(); + + private Builder(Config config) { + // Add the default channel (which can be overridden in the config) + channel(DEFAULT_CHANNEL_NAME, GrpcChannelDescriptor.builder().build()); + + if (config == null) { + return; + } + + Config channelsConfig = config.get(CFG_KEY_CHANNELS); + if (channelsConfig.exists()) { + for (Config channelConfig : channelsConfig.asNodeList().get()) { + String key = channelConfig.key().name(); + GrpcChannelDescriptor cfg = GrpcChannelDescriptor.builder().config(channelConfig).build(); + channelConfigs.put(key, cfg); + } + } + } + + /** + * Add or replace the specified {@link GrpcChannelDescriptor}. + * + * @param name the name of the configuration + * @param descriptor the {@link GrpcChannelDescriptor} to be added + * @return this Builder instance + */ + public Builder channel(String name, GrpcChannelDescriptor descriptor) { + channelConfigs.put(name, descriptor); + return this; + } + + /** + * Create a new instance of {@link GrpcChannelsProvider} from this Builder. + * + * @return a new instance of {@link GrpcChannelsProvider} + */ + public GrpcChannelsProvider build() { + return new GrpcChannelsProvider(channelConfigs); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java new file mode 100644 index 00000000000..baf4ce5ebaf --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java @@ -0,0 +1,195 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.helidon.common.Builder; +import io.helidon.grpc.api.Grpc; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.microprofile.grpc.core.AbstractServiceBuilder; +import io.helidon.microprofile.grpc.core.AnnotatedMethod; +import io.helidon.microprofile.grpc.core.AnnotatedMethodList; +import io.helidon.microprofile.grpc.core.InstanceSupplier; +import io.helidon.microprofile.grpc.core.ModelHelper; + +import static java.lang.System.Logger.Level; + +/** + * A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} instances + * from an annotated POJO. + */ +class GrpcClientBuilder extends AbstractServiceBuilder + implements Builder { + + private static final System.Logger LOGGER = System.getLogger(GrpcClientBuilder.class.getName()); + + /** + * Create a {@link GrpcClientBuilder} for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @param instance the target instance to call gRPC handler methods on + * @throws NullPointerException if the service or instance parameters are null + */ + private GrpcClientBuilder(Class serviceClass, Supplier instance) { + super(serviceClass, instance); + } + + /** + * Create a {@link GrpcClientBuilder} for a given gRPC service. + * + * @param service the service to call gRPC handler methods on + * @throws NullPointerException if the service is null + * @return a {@link GrpcClientBuilder} + */ + static GrpcClientBuilder create(Object service) { + return new GrpcClientBuilder(service.getClass(), InstanceSupplier.singleton(service)); + } + + /** + * Create a {@link GrpcClientBuilder} for a given gRPC service class. + * + * @param serviceClass gRPC service (handler) class. + * @throws NullPointerException if the service class is null + * @return a {@link GrpcClientBuilder} + */ + static GrpcClientBuilder create(Class serviceClass) { + return new GrpcClientBuilder(Objects.requireNonNull(serviceClass), createInstanceSupplier(serviceClass)); + } + + /** + * Create a new resource model builder for the introspected class. + *

+ * The model returned is filled with the introspected data. + *

+ * + * @return new resource model builder for the introspected class. + */ + @Override + public ClientServiceDescriptor.Builder build() { + checkForNonPublicMethodIssues(); + + Class annotatedServiceClass = annotatedServiceClass(); + AnnotatedMethodList methodList = AnnotatedMethodList.create(annotatedServiceClass); + String name = determineServiceName(annotatedServiceClass); + + ClientServiceDescriptor.Builder builder = ClientServiceDescriptor.builder(serviceClass()) + .name(name) + .marshallerSupplier(getMarshallerSupplier()); + + addServiceMethods(builder, methodList); + + LOGGER.log(Level.DEBUG, () -> String.format("A new gRPC service was created by ServiceModeller: %s", builder)); + + return builder; + } + + /** + * Add methods to the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder}. + * + * @param builder the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} to add the method to + * @param methodList the list of methods to add + */ + private void addServiceMethods(ClientServiceDescriptor.Builder builder, AnnotatedMethodList methodList) { + for (AnnotatedMethod am : methodList.withAnnotation(Grpc.GrpcMethod.class)) { + addServiceMethod(builder, am); + } + for (AnnotatedMethod am : methodList.withMetaAnnotation(Grpc.GrpcMethod.class)) { + addServiceMethod(builder, am); + } + } + + /** + * Add a method to the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder}. + *

+ * The method configuration will be determined by the annotations present on the + * method and the method signature. + * + * @param builder the {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} to add the method to + * @param method the {@link io.helidon.microprofile.grpc.core.AnnotatedMethod} representing the method to add + */ + private void addServiceMethod(ClientServiceDescriptor.Builder builder, AnnotatedMethod method) { + Grpc.GrpcMethod annotation = method.firstAnnotationOrMetaAnnotation(Grpc.GrpcMethod.class); + String name = determineMethodName(method, annotation); + + MethodHandler handler = handlerSuppliers().stream() + .filter(supplier -> supplier.supplies(method)) + .findFirst() + .map(supplier -> supplier.get(name, method, instanceSupplier())) + .orElseThrow(() -> new IllegalArgumentException("Cannot locate a method handler supplier for method " + method)); + + Class requestType = handler.getRequestType(); + Class responseType = handler.getResponseType(); + AnnotatedMethodConfigurer configurer = new AnnotatedMethodConfigurer(method, requestType, responseType, handler); + + switch (annotation.value()) { + case UNARY: + builder.unary(name, configurer); + break; + case CLIENT_STREAMING: + builder.clientStreaming(name, configurer); + break; + case SERVER_STREAMING: + builder.serverStreaming(name, configurer); + break; + case BIDI_STREAMING: + builder.bidirectional(name, configurer); + break; + case UNKNOWN: + default: + LOGGER.log(Level.ERROR, () -> "Unrecognized method type " + annotation.value()); + } + } + + /** + * A {@link java.util.function.Consumer} of {@link io.helidon.microprofile.grpc.client.ClientMethodDescriptor.Rules} + * that applies configuration changes based on annotations present on the gRPC + * method. + */ + private static class AnnotatedMethodConfigurer + implements Consumer { + + private final AnnotatedMethod method; + private final Class requestType; + private final Class responseType; + private final MethodHandler methodHandler; + + private AnnotatedMethodConfigurer(AnnotatedMethod method, + Class requestType, + Class responseType, + MethodHandler methodHandler) { + this.method = method; + this.requestType = requestType; + this.responseType = responseType; + this.methodHandler = methodHandler; + } + + @Override + public void accept(ClientMethodDescriptor.Rules config) { + config.requestType(requestType) + .responseType(responseType) + .methodHandler(methodHandler); + + if (method.isAnnotationPresent(Grpc.GrpcMarshaller.class)) { + config.marshallerSupplier(ModelHelper.getMarshallerSupplier( + method.getAnnotation(Grpc.GrpcMarshaller.class))); + } + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java new file mode 100644 index 00000000000..d0ec1033a71 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientCdiExtension.java @@ -0,0 +1,107 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +import io.helidon.grpc.api.Grpc; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedMethod; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.BeanAttributes; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessInjectionPoint; +import jakarta.enterprise.inject.spi.ProducerFactory; + +/** + * A CDI extension to add gRPC client functionality. + */ +public class GrpcClientCdiExtension implements Extension { + + private final Set proxyTypes = new HashSet<>(); + + /** + * Adds beans to the bean manager. + * + * @param event before bean discovery event + */ + public void addBeans(@Observes BeforeBeanDiscovery event) { + event.addAnnotatedType(ChannelProducer.class, ChannelProducer.class.getName()); + } + + /** + * Process injection points. + *

+ * In this method injection points that have the {@link io.helidon.grpc.api.Grpc.GrpcProxy} are processed + * and their types are stored so that in the {@link #afterBean( + *jakarta.enterprise.inject.spi.AfterBeanDiscovery, jakarta.enterprise.inject.spi.BeanManager)} + * we can manually create a producer for the correct service proxy type. + * + * @param pip the injection point + * @param the declared type of the injection point. + * @param the bean class of the bean that declares the injection point + */ + public void gatherApplications(@Observes ProcessInjectionPoint pip) { + Annotated annotated = pip.getInjectionPoint().getAnnotated(); + if (annotated.isAnnotationPresent(Grpc.GrpcProxy.class)) { + Type type = pip.getInjectionPoint().getType(); + proxyTypes.add(type); + } + } + + /** + * Process the previously captured {@link io.helidon.grpc.api.Grpc.GrpcProxy} injection points. + *

+ * For each {@link io.helidon.grpc.api.Grpc.GrpcProxy} injection point we create a producer bean + * for the required type. + * + * @param event the {@link jakarta.enterprise.inject.spi.AfterBeanDiscovery} event + * @param beanManager the CDI bean manager + */ + public void afterBean(@Observes AfterBeanDiscovery event, BeanManager beanManager) { + AnnotatedType producerType = beanManager.createAnnotatedType(GrpcProxyProducer.class); + AnnotatedMethod producerMethod = producerType.getMethods() + .stream() + .filter(m -> m.isAnnotationPresent(Grpc.GrpcProxy.class)) + .filter(m -> m.isAnnotationPresent(Grpc.GrpcChannel.class)) + .findFirst() + .orElse(null); + if (producerMethod != null) { + for (Type type : proxyTypes) { + addProducerBean(event, beanManager, producerMethod, type); + } + } + } + + private void addProducerBean(AfterBeanDiscovery event, + BeanManager beanManager, + AnnotatedMethod producerMethod, + Type type) { + BeanAttributes producerAttributes = beanManager.createBeanAttributes(producerMethod); + ProducerFactory factory = beanManager.getProducerFactory(producerMethod, null); + Set types = Set.of(Object.class, type); + BeanAttributes beanAttributes = DelegatingBeanAttributes.create(producerAttributes, types); + event.addBean(beanManager.createBean(beanAttributes, GrpcProxyProducer.class, factory)); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java new file mode 100644 index 00000000000..a1fb6b29151 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java @@ -0,0 +1,79 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.helidon.common.Builder; + +import io.grpc.Channel; + +/** + * A builder for gRPC clients dynamic proxies. + * + * @param the type of the interface to be proxied + */ +public class GrpcProxyBuilder implements Builder, T> { + + private static final Map, ClientServiceDescriptor> DESCRIPTORS = new ConcurrentHashMap<>(); + + private final GrpcServiceClient client; + + private final Class type; + + private GrpcProxyBuilder(GrpcServiceClient client, Class type) { + this.client = client; + this.type = type; + } + + /** + * Create a {@code GrpcProxyBuilder} that can build gRPC dynamic proxies + * for a given gRPC service interface. + *

+ * The class passed to this method should be properly annotated with + * {@link io.helidon.grpc.api.Grpc.GrpcService} and + * {@link io.helidon.grpc.api.Grpc.GrpcMethod} annotations + * so that the proxy can properly route calls to the server. + * + * @param channel the {@link io.grpc.Channel} to connect to the server + * @param type the service type + * @param the service type + * @return a {@link io.helidon.microprofile.grpc.client.GrpcProxyBuilder} that can build dynamic proxies + * for the gRPC service + */ + public static GrpcProxyBuilder create(Channel channel, Class type) { + ClientServiceDescriptor descriptor = DESCRIPTORS.computeIfAbsent(type, GrpcProxyBuilder::createDescriptor); + return new GrpcProxyBuilder<>(GrpcServiceClient.builder(channel, descriptor).build(), type); + } + + /** + * Build a gRPC client dynamic proxy of the required type. + * + * @return a gRPC client dynamic proxy + */ + @Override + public T build() { + return client.proxy(type); + } + + private static ClientServiceDescriptor createDescriptor(Class type) { + GrpcClientBuilder builder = GrpcClientBuilder.create(type); + ClientServiceDescriptor.Builder descriptorBuilder = builder.build(); + return descriptorBuilder.build(); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java new file mode 100644 index 00000000000..1b2c4a41535 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java @@ -0,0 +1,69 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import io.helidon.grpc.api.Grpc; +import io.helidon.microprofile.grpc.core.ModelHelper; + +import io.grpc.Channel; +import jakarta.enterprise.inject.spi.InjectionPoint; + +/** + * A utility class of gRPC CDI producer stubs. + *

+ * The methods in this class are not real CDI producer methods, + * they act as templates that the {@link io.helidon.microprofile.grpc.client.GrpcClientCdiExtension} + * will use to create producers on the fly as injection points + * are observed. + */ +class GrpcProxyProducer { + + private GrpcProxyProducer() { + } + + /** + * A CDI producer method that produces a client proxy for a gRPC service that + * will connect to the server using the channel specified via + * {@link io.helidon.grpc.api.Grpc.GrpcChannel} annotation on the proxy interface + * or injection point, or the default {@link io.grpc.Channel}. + *

+ * This is not a real producer method but is used as a stub by the gRPC client + * CDI extension to create real producers as injection points are discovered. + * + * @param injectionPoint the injection point where the client proxy is to be injected + * @return a gRPC client proxy + */ + @Grpc.GrpcProxy + @Grpc.GrpcChannel(value = GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) + static Object proxyUsingNamedChannel(InjectionPoint injectionPoint, ChannelProducer producer) { + Class type = ModelHelper.getGenericType(injectionPoint.getType()); + + String channelName; + if (injectionPoint.getAnnotated().isAnnotationPresent(Grpc.GrpcChannel.class)) { + channelName = injectionPoint.getAnnotated().getAnnotation(Grpc.GrpcChannel.class).value(); + } else { + channelName = type.isAnnotationPresent(Grpc.GrpcChannel.class) + ? type.getAnnotation(Grpc.GrpcChannel.class).value() + : GrpcChannelsProvider.DEFAULT_CHANNEL_NAME; + } + + Channel channel = producer.findChannel(channelName); + GrpcProxyBuilder builder = GrpcProxyBuilder.create(channel, type); + + return builder.build(); + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java new file mode 100644 index 00000000000..8bb65eb4b03 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java @@ -0,0 +1,471 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import io.helidon.grpc.core.InterceptorWeights; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.grpc.core.WeightedBag; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor.MethodType; +import io.grpc.Status; +import io.grpc.stub.AbstractStub; +import io.grpc.stub.ClientCalls; +import io.grpc.stub.StreamObserver; + +/** + * A gRPC Client for a specific gRPC service. + */ +public class GrpcServiceClient { + + private final HashMap> methodStubs; + + private final ClientServiceDescriptor clientServiceDescriptor; + + /** + * Creates a {@link Builder}. + * + * @param channel the {@link Channel} to use to connect to the server + * @param descriptor the {@link ClientServiceDescriptor} describing the gRPC service + * + * @return a new instance of {@link Builder} + */ + public static Builder builder(Channel channel, ClientServiceDescriptor descriptor) { + return new Builder(channel, descriptor); + } + + /** + * Creates a {@link GrpcServiceClient}. + * + * @param channel the {@link Channel} to use to connect to the server + * @param descriptor the {@link ClientServiceDescriptor} describing the gRPC service + * + * @return a new instance of {@link Builder} + */ + public static GrpcServiceClient create(Channel channel, ClientServiceDescriptor descriptor) { + return builder(channel, descriptor).build(); + } + + private GrpcServiceClient(Channel channel, + CallOptions callOptions, + ClientServiceDescriptor clientServiceDescriptor) { + this.clientServiceDescriptor = clientServiceDescriptor; + this.methodStubs = new HashMap<>(); + + // Merge Interceptors specified in Channel, ClientServiceDescriptor and ClientMethodDescriptor. + // Add the merged interceptor list to the AbstractStub which will be be used for the invocation + // of the method. + for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) { + GrpcMethodStub methodStub = new GrpcMethodStub<>(channel, callOptions, methodDescriptor); + + WeightedBag priorityInterceptors = WeightedBag.create(InterceptorWeights.USER); + priorityInterceptors.addAll(clientServiceDescriptor.interceptors()); + priorityInterceptors.addAll(methodDescriptor.interceptors()); + List interceptors = priorityInterceptors.stream().toList(); + + if (!interceptors.isEmpty()) { + LinkedHashSet uniqueInterceptors = new LinkedHashSet<>(interceptors.size()); + + // iterate the interceptors in reverse order so that the interceptor chain is in the correct order + for (int i = interceptors.size() - 1; i >= 0; i--) { + ClientInterceptor interceptor = interceptors.get(i); + if (!uniqueInterceptors.contains(interceptor)) { + uniqueInterceptors.add(interceptor); + } + } + + for (ClientInterceptor interceptor : uniqueInterceptors) { + methodStub = methodStub.withInterceptors(interceptor); + } + } + + if (methodDescriptor.callCredentials() != null) { + // Method level CallCredentials take precedence over service level CallCredentials. + methodStub = methodStub.withCallCredentials(methodDescriptor.callCredentials()); + } else if (clientServiceDescriptor.callCredentials() != null) { + methodStub = methodStub.withCallCredentials(clientServiceDescriptor.callCredentials()); + } + + methodStubs.put(methodDescriptor.name(), methodStub); + } + } + + /** + * Obtain the service name. + * + * @return The name of the service + */ + public String serviceName() { + return clientServiceDescriptor.name(); + } + + /** + * Invoke the specified method using the method's + * {@link MethodHandler}. + * + * @param name the name of the method to invoke + * @param args the method arguments + * @return the method response + */ + Object invoke(String name, Object[] args) { + GrpcMethodStub stub = methodStubs.get(name); + if (stub == null) { + throw Status.INTERNAL.withDescription("gRPC method '" + name + "' does not exist").asRuntimeException(); + } + ClientMethodDescriptor descriptor = stub.descriptor(); + MethodHandler methodHandler = descriptor.methodHandler(); + + return switch (descriptor.descriptor().getType()) { + case UNARY -> methodHandler.unary(args, this::unary); + case CLIENT_STREAMING -> methodHandler.clientStreaming(args, this::clientStreaming); + case SERVER_STREAMING -> methodHandler.serverStreaming(args, this::serverStreaming); + case BIDI_STREAMING -> methodHandler.bidirectional(args, this::bidiStreaming); + default -> throw Status.INTERNAL.withDescription("Unknown or unsupported method type for method " + name) + .asRuntimeException(); + }; + } + + /** + * Create a dynamic proxy for the specified interface that proxies + * calls to the wrapped gRPC service. + * + * @param type the interface to create a proxy for + * @param extraTypes extra types for the proxy to implement + * @param the type of the returned proxy + * @return a dynamic proxy that calls methods on this gRPC service + */ + @SuppressWarnings("unchecked") + public T proxy(Class type, Class... extraTypes) { + Map names = new HashMap<>(); + for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) { + MethodHandler methodHandler = methodDescriptor.methodHandler(); + if (methodHandler != null) { + names.put(methodHandler.javaMethodName(), methodDescriptor.name()); + } + } + + Class[] proxyTypes; + if (extraTypes == null || extraTypes.length == 0) { + proxyTypes = new Class[] {type}; + } else { + proxyTypes = new Class[extraTypes.length + 1]; + proxyTypes[0] = type; + System.arraycopy(extraTypes, 0, proxyTypes, 1, extraTypes.length); + } + return (T) Proxy.newProxyInstance(type.getClassLoader(), proxyTypes, ClientProxy.create(this, names)); + } + + /** + * Invoke the specified unary method with the specified request object. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param the request type + * @param the response type + * + * @return The result of this invocation + */ + public RespT blockingUnary(String methodName, ReqT request) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY); + return ClientCalls.blockingUnaryCall( + stub.getChannel(), stub.descriptor().descriptor(), stub.getCallOptions(), request); + } + + /** + * Asynchronously invoke the specified unary method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param the request type + * @param the response type + * + * @return A {@link CompletionStage} that will complete with the result of the unary method call + */ + public CompletionStage unary(String methodName, ReqT request) { + SingleValueStreamObserver observer = new SingleValueStreamObserver<>(); + + GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY); + ClientCalls.asyncUnaryCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request, + observer); + + return observer.completionStage(); + } + + /** + * Invoke the specified unary method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param observer a {@link StreamObserver} to receive the result + * @param the request type + * @param the response type + */ + public void unary(String methodName, ReqT request, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.UNARY); + ClientCalls.asyncUnaryCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request, + observer); + } + + /** + * Invoke the specified server streaming method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param the request type + * @param the response type + * + * @return an {@link Iterator} to obtain the streamed results + */ + public Iterator blockingServerStreaming(String methodName, ReqT request) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.SERVER_STREAMING); + return ClientCalls.blockingServerStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request); + } + + /** + * Invoke the specified server streaming method. + * + * @param methodName the method name to be invoked + * @param request the request parameter + * @param observer a {@link StreamObserver} to receive the results + * @param the request type + * @param the response type + */ + public void serverStreaming(String methodName, ReqT request, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.SERVER_STREAMING); + ClientCalls.asyncServerStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + request, + observer); + } + + /** + * Invoke the specified client streaming method. + * + * @param methodName the method name to be invoked + * @param items an {@link Iterable} of items to be streamed to the server + * @param the request type + * @param the response type + * @return A {@link StreamObserver} to retrieve the method call result + */ + public CompletionStage clientStreaming(String methodName, Iterable items) { + return clientStreaming(methodName, StreamSupport.stream(items.spliterator(), false)); + } + + /** + * Invoke the specified client streaming method. + * + * @param methodName the method name to be invoked + * @param items a {@link Stream} of items to be streamed to the server + * @param the request type + * @param the response type + * @return A {@link StreamObserver} to retrieve the method call result + */ + public CompletionStage clientStreaming(String methodName, Stream items) { + SingleValueStreamObserver obsv = new SingleValueStreamObserver<>(); + GrpcMethodStub stub = ensureMethod(methodName, MethodType.CLIENT_STREAMING); + StreamObserver reqStream = ClientCalls.asyncClientStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + obsv); + + items.forEach(reqStream::onNext); + reqStream.onCompleted(); + + return obsv.completionStage(); + } + + /** + * Invoke the specified client streaming method. + * + * @param methodName the method name to be invoked + * @param observer a {@link StreamObserver} to receive the result + * @param the request type + * @param the response type + * @return a {@link StreamObserver} to use to stream requests to the server + */ + public StreamObserver clientStreaming(String methodName, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.CLIENT_STREAMING); + return ClientCalls.asyncClientStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + observer); + } + + /** + * Invoke the specified bidirectional streaming method. + * + * @param methodName the method name to be invoked. + * @param observer a {@link StreamObserver} to receive the result + * @param the request type + * @param the response type + * @return A {@link StreamObserver} to use to stream requests to the server + */ + public StreamObserver bidiStreaming(String methodName, StreamObserver observer) { + GrpcMethodStub stub = ensureMethod(methodName, MethodType.BIDI_STREAMING); + return ClientCalls.asyncBidiStreamingCall( + stub.getChannel().newCall(stub.descriptor().descriptor(), stub.getCallOptions()), + observer); + } + + @SuppressWarnings("unchecked") + private GrpcMethodStub ensureMethod(String methodName, MethodType methodType) { + GrpcMethodStub stub = (GrpcMethodStub) methodStubs.get(methodName); + if (stub == null) { + throw new IllegalArgumentException("No method named " + methodName + " registered with this service"); + } + ClientMethodDescriptor cmd = stub.descriptor(); + if (cmd.descriptor().getType() != methodType) { + throw new IllegalArgumentException("Method (" + methodName + ") already registered with a different method type."); + } + + return stub; + } + + /** + * GrpcMethodStub can be used to configure method specific Interceptors, Metrics, Tracing, Deadlines, etc. + */ + private static class GrpcMethodStub + extends AbstractStub> { + + private final ClientMethodDescriptor cmd; + + GrpcMethodStub(Channel channel, CallOptions callOptions, ClientMethodDescriptor cmd) { + super(channel, callOptions); + this.cmd = cmd; + } + + @Override + protected GrpcMethodStub build(Channel channel, CallOptions callOptions) { + return new GrpcMethodStub<>(channel, callOptions, cmd); + } + + public ClientMethodDescriptor descriptor() { + return cmd; + } + } + + /** + * Builder to build an instance of {@link GrpcServiceClient}. + */ + public static class Builder { + + private final Channel channel; + + private CallOptions callOptions = CallOptions.DEFAULT; + + private final ClientServiceDescriptor clientServiceDescriptor; + + private Builder(Channel channel, ClientServiceDescriptor descriptor) { + this.channel = channel; + this.clientServiceDescriptor = descriptor; + } + + /** + * Set the {@link CallOptions} to use. + * + * @param callOptions the {@link CallOptions} to use + * @return This {@link Builder} for fluent method chaining + */ + public Builder callOptions(CallOptions callOptions) { + this.callOptions = callOptions; + return this; + } + + /** + * Build an instance of {@link GrpcServiceClient}. + * + * @return an new instance of a {@link GrpcServiceClient} + */ + public GrpcServiceClient build() { + return new GrpcServiceClient(channel, callOptions, clientServiceDescriptor); + } + } + + /** + * A simple {@link StreamObserver} adapter class that completes + * a {@link CompletableFuture} when the observer is completed. + *

+ * This observer uses the value passed to its {@link #onNext(Object)} method to complete + * the {@link CompletableFuture}. + *

+ * This observer should only be used in cases where a single result is expected. If more + * that one call is made to {@link #onNext(Object)} then future will be completed with + * an exception. + * + * @param The type of objects received in this stream. + */ + public static class SingleValueStreamObserver implements StreamObserver { + + private int count; + + private T result; + + private final CompletableFuture resultFuture = new CompletableFuture<>(); + + /** + * Create a SingleValueStreamObserver. + */ + public SingleValueStreamObserver() { + } + + /** + * Obtain the {@link CompletableFuture} that will be completed + * when the {@link StreamObserver} completes. + * + * @return The CompletableFuture + */ + public CompletionStage completionStage() { + return resultFuture; + } + + @Override + public void onNext(T value) { + if (count++ == 0) { + result = value; + } else { + resultFuture.completeExceptionally(new IllegalStateException("More than one result received.")); + } + } + + @Override + public void onError(Throwable t) { + resultFuture.completeExceptionally(t); + } + + @Override + public void onCompleted() { + resultFuture.complete(result); + } + } +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java new file mode 100644 index 00000000000..a3232b59001 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Client gRPC microprofile classes. + */ +package io.helidon.microprofile.grpc.client; diff --git a/microprofile/grpc/client/src/main/java/module-info.java b/microprofile/grpc/client/src/main/java/module-info.java new file mode 100644 index 00000000000..02a914828d5 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/module-info.java @@ -0,0 +1,41 @@ +/* + * 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. + */ + +/** + * gRPC microprofile client module. + */ +module io.helidon.microprofile.grpc.client { + + requires io.helidon.common; + requires io.helidon.common.tls; + requires io.helidon.tracing; + requires io.helidon.config; + requires io.helidon.webclient.grpc; + requires io.helidon.grpc.api; + requires io.helidon.microprofile.grpc.core; + + requires io.grpc; + requires jakarta.cdi; + requires jakarta.inject; + + requires transitive io.helidon.grpc.core; + + exports io.helidon.microprofile.grpc.client; + + provides jakarta.enterprise.inject.spi.Extension with + io.helidon.microprofile.grpc.client.GrpcClientCdiExtension; + +} \ No newline at end of file diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java new file mode 100644 index 00000000000..fc4b49190d8 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientMethodDescriptorTest.java @@ -0,0 +1,139 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import io.helidon.microprofile.grpc.client.test.Echo; +import io.helidon.microprofile.grpc.client.test.EchoServiceGrpc; + +import io.grpc.MethodDescriptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; + +public class ClientMethodDescriptorTest { + + private MethodDescriptor.Builder grpcDescriptor; + + @BeforeEach + public void setup() { + grpcDescriptor = EchoServiceGrpc.getServiceDescriptor() + .getMethods() + .stream() + .filter(md -> md.getFullMethodName().equals("EchoService/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find echo method")) + .toBuilder(); + } + + @Test + public void shouldCreateMethodDescriptorFromGrpcDescriptor() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.create("FooService", + "foo", + grpcDescriptor); + + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + assertThat(descriptor.interceptors(), is(emptyIterable())); + + MethodDescriptor expected = grpcDescriptor.build(); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(expected.getType())); + assertThat(methodDescriptor.getRequestMarshaller(), is(expected.getRequestMarshaller())); + assertThat(methodDescriptor.getResponseMarshaller(), is(expected.getResponseMarshaller())); + } + + @Test + public void shouldCreateBidirectionalMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.bidirectional("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING)); + } + + @Test + public void shouldCreateClientStreamingMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.clientStreaming("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING)); + } + + @Test + public void shouldCreateServerStreamingMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.serverStreaming("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING)); + } + + @Test + public void shouldCreateUnaryMethod() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor.unary("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()) + .build(); + assertThat(descriptor, is(notNullValue())); + assertThat(descriptor.name(), is("foo")); + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getFullMethodName(), is("FooService/foo")); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY)); + } + + @Test + public void shouldSetName() { + ClientMethodDescriptor.Builder builder = ClientMethodDescriptor + .unary("FooService", "foo") + .defaultMarshallerSupplier(new JavaMarshaller.Supplier()); + + builder.fullName("Foo/Bar"); + + ClientMethodDescriptor descriptor = builder.build(); + + assertThat(descriptor.name(), is("Bar")); + assertThat(descriptor.descriptor().getFullMethodName(), is("Foo/Bar")); + } + + @Test + public void testMarshallerTypesForProtoBuilder() { + ClientMethodDescriptor descriptor = ClientMethodDescriptor + .unary("EchoService", "Echo") + .requestType(Echo.EchoRequest.class) + .responseType(Echo.EchoResponse.class) + .build(); + + MethodDescriptor methodDescriptor = descriptor.descriptor(); + assertThat(methodDescriptor.getRequestMarshaller(), instanceOf(MethodDescriptor.PrototypeMarshaller.class)); + assertThat(methodDescriptor.getResponseMarshaller(), instanceOf(MethodDescriptor.PrototypeMarshaller.class)); + } +} diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java new file mode 100644 index 00000000000..7d945d45fd3 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/ClientServiceDescriptorTest.java @@ -0,0 +1,292 @@ +/* + * 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. + * 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.microprofile.grpc.client; + +import java.util.Collection; + +import io.helidon.microprofile.grpc.client.test.StringServiceGrpc; + +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServiceDescriptor; +import org.junit.jupiter.api.Test; +import services.TreeMapService; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +public class ClientServiceDescriptorTest { + + @Test + public void shouldCreateDescriptorFromGrpcServiceDescriptor() { + ServiceDescriptor grpcDescriptor = StringServiceGrpc.getServiceDescriptor(); + ClientServiceDescriptor descriptor = ClientServiceDescriptor.create(grpcDescriptor); + String serviceName = "StringService"; + assertThat(descriptor.name(), is(serviceName)); + assertThat(descriptor.interceptors(), is(emptyIterable())); + + Collection> expectedMethods = grpcDescriptor.getMethods(); + Collection actualMethods = descriptor.methods(); + assertThat(actualMethods.size(), is(expectedMethods.size())); + + for (MethodDescriptor methodDescriptor : expectedMethods) { + String name = methodDescriptor.getFullMethodName().substring(serviceName.length() + 1); + ClientMethodDescriptor method = descriptor.method(name); + assertThat(method.name(), is(name)); + assertThat(method.interceptors(), is(emptyIterable())); + MethodDescriptor actualDescriptor = method.descriptor(); + assertThat(actualDescriptor.getType(), is(methodDescriptor.getType())); + } + } + + @Test + public void shouldCreateDescriptorFromBindableService() { + StringServiceBindableService bindableService = new StringServiceBindableService(); + ClientServiceDescriptor descriptor = ClientServiceDescriptor.create(bindableService); + String serviceName = "StringService"; + assertThat(descriptor.name(), is(serviceName)); + assertThat(descriptor.interceptors(), is(emptyIterable())); + + Collection> expectedMethods = bindableService.bindService().getMethods(); + Collection actualMethods = descriptor.methods(); + assertThat(actualMethods.size(), is(expectedMethods.size())); + + for (ServerMethodDefinition expectedMethod : expectedMethods) { + MethodDescriptor methodDescriptor = expectedMethod.getMethodDescriptor(); + String name = methodDescriptor.getFullMethodName().substring(serviceName.length() + 1); + ClientMethodDescriptor method = descriptor.method(name); + assertThat(method.name(), is(name)); + assertThat(method.interceptors(), is(emptyIterable())); + MethodDescriptor actualDescriptor = method.descriptor(); + assertThat(actualDescriptor.getType(), is(methodDescriptor.getType())); + } + } + + @Test + public void testServiceName() { + ClientServiceDescriptor.Builder builder = ClientServiceDescriptor.builder("TreeMapService", + TreeMapService.class); + assertThat(builder.name(), is("TreeMapService")); + + ClientServiceDescriptor descriptor = builder.build(); + assertThat(descriptor.name(), is("TreeMapService")); + } + + @Test + public void testDefaultMethodCount() { + ClientServiceDescriptor svcDesc = newClientServiceDescriptorBuilder(TreeMapService.class).build(); + assertThat(svcDesc.methods().size(), equalTo(0)); + } + + @Test + public void shouldNotAllowNullName() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + assertThrows(NullPointerException.class, () -> builder.name(null)); + } + + @Test + public void shouldNotAllowEmptyStringName() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + assertThrows(IllegalArgumentException.class, () -> builder.name("")); + } + + @Test + public void shouldNotAllowBlankName() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + assertThrows(IllegalArgumentException.class, () -> builder.name(" \t ")); + } + + @Test + public void shouldAddBidirectionalMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .bidirectional("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddBidirectionalMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .bidirectional("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.BIDI_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddClientStreamingMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .clientStreaming("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddClientStreamingMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .clientStreaming("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.CLIENT_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddServerStreamingMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .serverStreaming("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddServerStreamingMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .serverStreaming("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.SERVER_STREAMING)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddUnaryMethod() { + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .unary("foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddUnaryMethodWithConfigurer() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .unary("foo", cfg -> cfg.intercept(interceptor)) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + MethodDescriptor methodDescriptor = method.descriptor(); + assertThat(methodDescriptor.getType(), is(MethodDescriptor.MethodType.UNARY)); + assertThat(methodDescriptor.getFullMethodName(), is("TreeMapService/foo")); + } + + @Test + public void shouldAddInterceptor() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .intercept(interceptor) + .build(); + + assertThat(descriptor.interceptors(), contains(interceptor)); + } + + @Test + public void shouldAddInterceptors() { + ClientInterceptor interceptorOne = mock(ClientInterceptor.class); + ClientInterceptor interceptorTwo = mock(ClientInterceptor.class); + ClientInterceptor interceptorThree = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .intercept(interceptorOne) + .intercept(interceptorTwo, interceptorThree) + .build(); + + assertThat(descriptor.interceptors(), containsInAnyOrder(interceptorOne, interceptorTwo, interceptorThree)); + } + + @Test + public void shouldAddInterceptorToMethod() { + ClientInterceptor interceptor = mock(ClientInterceptor.class); + ClientServiceDescriptor descriptor = newClientServiceDescriptorBuilder(TreeMapService.class) + .unary("foo") + .intercept("foo", interceptor) + .build(); + + ClientMethodDescriptor method = descriptor.method("foo"); + assertThat(method, is(notNullValue())); + assertThat(method.interceptors(), contains(interceptor)); + } + + @Test + public void shouldSetNameOnMethods() { + ClientServiceDescriptor.Builder builder = newClientServiceDescriptorBuilder(TreeMapService.class); + + ClientServiceDescriptor descriptor = builder.unary("bar") + .name("Foo") + .build(); + + ClientMethodDescriptor method = descriptor.method("bar"); + assertThat(method.descriptor().getFullMethodName(), is("Foo/bar")); + } + + public static class StringServiceBindableService + extends StringServiceGrpc.StringServiceImplBase { + } + + private ClientServiceDescriptor.Builder newClientServiceDescriptorBuilder(Class service) { + return ClientServiceDescriptor.builder(service) + .marshallerSupplier(new JavaMarshaller.Supplier()); + } +} diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java new file mode 100644 index 00000000000..b30420d021f --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java @@ -0,0 +1,139 @@ +/* + * 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.microprofile.grpc.client; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.grpc.api.Grpc; +import io.helidon.microprofile.grpc.server.GrpcMpCdiExtension; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.webclient.grpc.GrpcClient; + +import io.grpc.stub.StreamObserver; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.Test; + +import static io.helidon.grpc.core.ResponseHelper.complete; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddBean(EchoServiceTest.EchoService.class) +@AddBean(JavaMarshaller.Supplier.class) +@AddExtension(GrpcMpCdiExtension.class) +@AddExtension(GrpcClientCdiExtension.class) +class EchoServiceTest { + + @Inject + private WebTarget webTarget; + + @Inject + @Grpc.GrpcProxy + private EchoServiceClient proxyClient; + + @Test + void testEcho() throws InterruptedException, ExecutionException, TimeoutException { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + webTarget.getUri().getPort()) + .build(); + + ClientServiceDescriptor descriptor = ClientServiceDescriptor.builder(EchoService.class) + .name("EchoService") + .marshallerSupplier(new JavaMarshaller.Supplier()) + .unary("Echo") + .build(); + + CompletableFuture future = new CompletableFuture<>(); + GrpcServiceClient client = GrpcServiceClient.create(grpcClient.channel(), descriptor); + StreamObserver observer = new StreamObserver<>() { + @Override + public void onNext(String value) { + future.complete(value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }; + client.unary("Echo", "Howdy", observer); + assertThat(future.get(5, TimeUnit.SECONDS), is("Howdy")); + } + + @Test + void testEchoInject() throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver observer = new StreamObserver<>() { + @Override + public void onNext(String value) { + future.complete(value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }; + proxyClient.echo("Howdy", observer); + assertThat(future.get(5, TimeUnit.SECONDS), is("Howdy")); + } + + @Grpc.GrpcService + @Grpc.GrpcMarshaller("java") + public static class EchoService { + + @Grpc.Unary("Echo") + public void echo(String request, StreamObserver observer) { + try { + complete(observer, request); + } catch (IllegalStateException e) { + observer.onError(e); + } + } + } + + @Grpc.GrpcService("EchoService") + @Grpc.GrpcMarshaller("java") + @Grpc.GrpcChannel(value = "echo-channel") + public interface EchoServiceClient { + + @Grpc.Unary("Echo") + void echo(String request, StreamObserver observer); + } +} diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java new file mode 100644 index 00000000000..de585accde5 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/JavaMarshaller.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022, 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.microprofile.grpc.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import io.helidon.grpc.core.MarshallerSupplier; + +import io.grpc.MethodDescriptor; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Named; + +/** + * An implementation of a gRPC {@link io.grpc.MethodDescriptor.Marshaller} that + * uses Java serialization for testing. + */ +public class JavaMarshaller implements MethodDescriptor.Marshaller { + + @Override + public InputStream stream(T obj) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out)) { + oos.writeObject(obj); + return new ByteArrayInputStream(out.toByteArray()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + @SuppressWarnings("unchecked") + public T parse(InputStream in) { + try (ObjectInputStream ois = new ObjectInputStream(in)) { + return (T) ois.readObject(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * A {@link io.helidon.grpc.core.MarshallerSupplier} implementation that supplies + * instances of {@link io.helidon.microprofile.grpc.client.JavaMarshaller}. + */ + @Dependent + @Named("java") + public static class Supplier implements MarshallerSupplier { + + @Override + public MethodDescriptor.Marshaller get(Class clazz) { + return new JavaMarshaller<>(); + } + } +} diff --git a/microprofile/grpc/client/src/test/java/services/EchoService.java b/microprofile/grpc/client/src/test/java/services/EchoService.java new file mode 100644 index 00000000000..a930b24555a --- /dev/null +++ b/microprofile/grpc/client/src/test/java/services/EchoService.java @@ -0,0 +1,53 @@ +/* + * 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. + * 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 services; + +import io.helidon.microprofile.grpc.client.test.Echo; +import io.helidon.webserver.grpc.GrpcService; + +import com.google.protobuf.Descriptors; +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.core.ResponseHelper.complete; + +/** + * A simple test gRPC echo service. + */ +public class EchoService implements GrpcService { + + @Override + public Descriptors.FileDescriptor proto() { + return Echo.getDescriptor(); + } + + @Override + public void update(Routing routing) { + routing.unary("Echo", this::echo); + } + + /** + * Echo the message back to the caller. + * + * @param request the echo request containing the message to echo + * @param observer the call response + */ + public void echo(Echo.EchoRequest request, StreamObserver observer) { + String message = request.getMessage(); + Echo.EchoResponse response = Echo.EchoResponse.newBuilder().setMessage(message).build(); + complete(observer, response); + } +} diff --git a/microprofile/grpc/client/src/test/java/services/TreeMapService.java b/microprofile/grpc/client/src/test/java/services/TreeMapService.java new file mode 100644 index 00000000000..973b87f8953 --- /dev/null +++ b/microprofile/grpc/client/src/test/java/services/TreeMapService.java @@ -0,0 +1,271 @@ +/* + * 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. + * 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 services; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; +import java.util.TreeMap; + +import io.helidon.webserver.grpc.GrpcService; + +import com.google.protobuf.Descriptors; +import io.grpc.stub.StreamObserver; + +import static io.helidon.grpc.core.ResponseHelper.complete; + +/** + * A simple class that implements gRPC service. Used for testing. + */ +public class TreeMapService implements GrpcService { + + /** + * A reference to a {@link services.TreeMapService.Person} named "Bilbo". + */ + public static Person BILBO = new Person(1, "Bilbo", 111, "Male", + new String[] {"Burglaring", "Pipe smoking"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Frodo". + */ + public static Person FRODO = new Person(2, "Frodo", 33, "Male", + new String[] {"Long hikes"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Aragon". + */ + public static Person ARAGON = new Person(3, "Aragon", 87, "Male", + new String[] {"Pipe smoking", "Hitting on elvish women"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Galadriel". + */ + public static Person GALARDRIEL = new Person(4, "Galadriel", 8372, "Female", + new String[] {"Dwarves"}); + + /** + * A reference to a {@link services.TreeMapService.Person} named "Gandalf". + */ + public static Person GANDALF = new Person(5, "Gandalf", 32767, "Male", + new String[] {"Wizardry"}); + + private final TreeMap lorMap = new TreeMap<>(); + + public TreeMapService() { + lorMap.put(1, BILBO); + lorMap.put(2, FRODO); + lorMap.put(3, ARAGON); + lorMap.put(4, GALARDRIEL); + lorMap.put(5, GANDALF); + } + + @Override + public Descriptors.FileDescriptor proto() { + return null; + } + + @Override + public void update(Routing routing) { + // TODO routing.marshallerSupplier(new JavaMarshaller.Supplier()); + routing.unary("get", this::get); + routing.serverStream("greaterOrEqualTo", this::greaterOrEqualTo); + routing.clientStream("sumOfAges", this::sumOfAges); + routing.bidi("persons", this::persons); + } + + /** + * Retrieve the person from the TreeMap. This is a UNARY call. + * + * @param id The id of the person. + * @param observer the call response + */ + public void get(Integer id, StreamObserver observer) { + complete(observer, lorMap.get(id)); + } + + /** + * Return the Persons whose Ids are greater than or equal to the specified key. This is a ServerStreaming call. + * + * @param id The id to use. + * @param observer A {@link io.grpc.stub.StreamObserver} into which {@link services.TreeMapService.Person}s whose ids + * are greater than or equal to the specified id will be emitted. + */ + public void greaterOrEqualTo(Integer id, StreamObserver observer) { + for (Person p : lorMap.tailMap(id).values()) { + observer.onNext(p); + } + observer.onCompleted(); + } + + /** + * Return the sum of ages of all Persons whose Ids are streamed from the client. This is a Client streaming call. + * + * @param observer A {@link io.grpc.stub.StreamObserver} into which the sum of ages of + * all {@link services.TreeMapService.Person}s will be emitted. + * @return A {@link io.grpc.stub.StreamObserver} into which the ids of {@link services.TreeMapService.Person}s + * should be emitted into. + */ + public StreamObserver sumOfAges(StreamObserver observer) { + return new StreamObserver() { + private int sum = 0; + + public void onNext(Integer id) { + System.out.println("Received id: ==> " + id); + Person p = lorMap.get(id); + sum += p != null ? p.age : 0; + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + observer.onNext(sum); + observer.onCompleted(); + } + }; + } + + /** + * Streams the {@link services.TreeMapService.Person} into the specified observer for each of the id that is + * streamed (from the client). This is a bi-directional streaming call. + * + * @param observer A {@link io.grpc.stub.StreamObserver} into which the sum of ages of + * all {@link services.TreeMapService.Person}s will be emitted. + * @return A {@link io.grpc.stub.StreamObserver} into which the ids of {@link services.TreeMapService.Person}s + * should be emitted into. + */ + public StreamObserver persons(StreamObserver observer) { + return new StreamObserver() { + public void onNext(Integer id) { + Person p = lorMap.get(id); + if (p != null) { + observer.onNext(p); + } + } + + public void onError(Throwable t) { + t.printStackTrace(); + } + + public void onCompleted() { + observer.onCompleted(); + } + }; + } + + /** + * A person class used in the test code. + */ + public static class Person implements Serializable { + + private final int id; + private final String name; + private final int age; + private final String gender; + private final String[] hobbies; + + /** + * Creates a new Person. + * @param id The id of the person. + * @param name The name of the person. + * @param age The age of the person. + * @param gender The gender of the person. + * @param hobbies The hobbies of the person. + */ + public Person(int id, String name, int age, String gender, String[] hobbies) { + this.id = id; + this.name = name; + this.age = age; + this.gender = gender; + this.hobbies = hobbies; + } + + /** + * Returns the Id of the person. + * @return The id of the person. + */ + public int getId() { + return id; + } + + /** + * Returns the name of the person. + * @return The name of the person. + */ + public String getName() { + return name; + } + + /** + * Returns the gender of the person. + * @return The gender of the person. + */ + public String getGender() { + return gender; + } + + /** + * Returns the hobbies of the person. + * @return The hobbies of the person. + */ + public String[] getHobbies() { + return hobbies; + } + + /** + * Returns the age of the person. + * @return The age of the person. + */ + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Person{" + + "id=" + id + + ", name='" + name + '\'' + + ", age=" + age + + ", gender='" + gender + '\'' + + ", hobbies=" + Arrays.toString(hobbies) + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Person person)) { + return false; + } + return getId() == person.getId() && + getAge() == person.getAge() && + Objects.equals(getName(), person.getName()) && + Objects.equals(getGender(), person.getGender()) && + Arrays.equals(getHobbies(), person.getHobbies()); + } + + @Override + public int hashCode() { + int result = Objects.hash(getId(), getName(), getAge(), getGender()); + result = 31 * result + Arrays.hashCode(getHobbies()); + return result; + } + } +} diff --git a/microprofile/grpc/client/src/test/proto/echo.proto b/microprofile/grpc/client/src/test/proto/echo.proto new file mode 100644 index 00000000000..68911ff5c12 --- /dev/null +++ b/microprofile/grpc/client/src/test/proto/echo.proto @@ -0,0 +1,30 @@ +/* + * 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. + * 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"; +option java_package = "io.helidon.microprofile.grpc.client.test"; + +service EchoService { + rpc Echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + string message = 1; +} + +message EchoResponse { + string message = 1; +} diff --git a/microprofile/grpc/client/src/test/proto/strings.proto b/microprofile/grpc/client/src/test/proto/strings.proto new file mode 100644 index 00000000000..8e01019e310 --- /dev/null +++ b/microprofile/grpc/client/src/test/proto/strings.proto @@ -0,0 +1,31 @@ +/* + * 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. + * 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"; +option java_package = "io.helidon.microprofile.grpc.client.test"; + +service StringService { + rpc Upper (StringMessage) returns (StringMessage) {} + rpc Lower (StringMessage) returns (StringMessage) {} + rpc Split (StringMessage) returns (stream StringMessage) {} + rpc Join (stream StringMessage) returns (StringMessage) {} + rpc Echo (stream StringMessage) returns (stream StringMessage) {} +} + +message StringMessage { + string text = 1; +} diff --git a/microprofile/grpc/client/src/test/resources/application.yaml b/microprofile/grpc/client/src/test/resources/application.yaml new file mode 100644 index 00000000000..bf73807e8ae --- /dev/null +++ b/microprofile/grpc/client/src/test/resources/application.yaml @@ -0,0 +1,47 @@ +# +# 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. +# + +server: + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "server.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "server.p12" + +grpc: + channels: + echo-channel: + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "client.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "client.p12" \ No newline at end of file diff --git a/microprofile/grpc/client/src/test/resources/client.p12 b/microprofile/grpc/client/src/test/resources/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..4eb3b8325cd0190163032aca86e1d4fc94856822 GIT binary patch literal 4181 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 U@+;F6i_J(6+nRj}N+2@-U-0Veh5!Hn literal 0 HcmV?d00001 diff --git a/microprofile/grpc/pom.xml b/microprofile/grpc/pom.xml index 286d044c437..89cb26d3b3f 100644 --- a/microprofile/grpc/pom.xml +++ b/microprofile/grpc/pom.xml @@ -34,6 +34,7 @@ core server + client tracing tests From 353c9f956938aa9e98fb5a9c9998e83187b3162b Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Jul 2024 12:32:41 -0400 Subject: [PATCH 02/12] Some minor cleanup. Signed-off-by: Santiago Pericas-Geertsen --- .../grpc/client/ChannelProducer.java | 2 +- .../grpc/client/GrpcChannelsProvider.java | 23 ++++++++++++------- .../grpc/client/GrpcProxyProducer.java | 2 +- .../grpc/client/EchoServiceTest.java | 2 +- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java index bcfc6d675ac..c29525e06b4 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java @@ -57,7 +57,7 @@ public class ChannelProducer { * @return a gRPC {@link io.grpc.Channel} */ @Produces - @Grpc.GrpcChannel(value = GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) + @Grpc.GrpcChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) public Channel get(InjectionPoint injectionPoint) { Grpc.GrpcChannel qualifier = injectionPoint.getQualifiers() .stream() diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java index db566648d0f..0ad6c5aeb3b 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java @@ -98,8 +98,6 @@ public static Builder builder(Config config) { return new Builder(config); } - // --------------- private methods of GrpcChannelsProvider --------------- - /** * Returns a {@link Channel} for the specified channel or host name. *

@@ -125,14 +123,21 @@ public Channel channel(String name) { return createChannel(name, chCfg); } - Channel createChannel(String name, GrpcChannelDescriptor descriptor) { + /** + * Creates a channel from a channel descriptor. + * + * @param name the channel name + * @param descriptor the channel descriptor + * @return the channel + */ + private Channel createChannel(String name, GrpcChannelDescriptor descriptor) { Tls clientTls = descriptor.tls().orElse(null); if (clientTls == null) { throw new IllegalArgumentException("Client TLS must be configured for gRPC proxy client"); } int port = descriptor.port(); if (port <= 0) { - port = discoverServerPort(); + port = discoverServerPort(name); } GrpcClient grpcClient = GrpcClient.builder() .tls(clientTls) @@ -142,13 +147,15 @@ Channel createChannel(String name, GrpcChannelDescriptor descriptor) { } /** - * Used for unit testing: if port not set, then obtain port from CDI extension - * via reflection. Should be moved to a testing module. + * Used for unit testing: if port not set, try to obtain port from server CDI + * extension via reflection, without introducing a static dependency. * + * @param name the channel name * @return server port + * @throws java.lang.IllegalArgumentException if unable to discover port */ @SuppressWarnings("unchecked") - private static int discoverServerPort() { + private static int discoverServerPort(String name) { try { Class extClass = (Class) Class .forName("io.helidon.microprofile.server.ServerCdiExtension"); @@ -156,7 +163,7 @@ private static int discoverServerPort() { Method m = extension.getClass().getMethod("port", String.class); return (int) m.invoke(extension, new Object[] {"@default"}); } catch (ReflectiveOperationException e) { - return 0; + throw new IllegalArgumentException("Unable to determine server port for channel " + name); } } diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java index 1b2c4a41535..20fc3f9d5fa 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyProducer.java @@ -48,7 +48,7 @@ private GrpcProxyProducer() { * @return a gRPC client proxy */ @Grpc.GrpcProxy - @Grpc.GrpcChannel(value = GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) + @Grpc.GrpcChannel(GrpcChannelsProvider.DEFAULT_CHANNEL_NAME) static Object proxyUsingNamedChannel(InjectionPoint injectionPoint, ChannelProducer producer) { Class type = ModelHelper.getGenericType(injectionPoint.getType()); diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java index b30420d021f..7d46dd392b5 100644 --- a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java @@ -130,7 +130,7 @@ public void echo(String request, StreamObserver observer) { @Grpc.GrpcService("EchoService") @Grpc.GrpcMarshaller("java") - @Grpc.GrpcChannel(value = "echo-channel") + @Grpc.GrpcChannel("echo-channel") public interface EchoServiceClient { @Grpc.Unary("Echo") From 7e078ab74b1e8fce720de1b07f1239b93b4b1595 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Jul 2024 12:45:07 -0400 Subject: [PATCH 03/12] Removes tracing interceptor. Tracing will be implemented at a later time. Signed-off-by: Santiago Pericas-Geertsen --- .../grpc/client/ClientTracingInterceptor.java | 409 ------------------ 1 file changed, 409 deletions(-) delete mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java deleted file mode 100644 index 8a2278fe5b4..00000000000 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ClientTracingInterceptor.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * 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. - * 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.microprofile.grpc.client; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import io.helidon.common.Weight; -import io.helidon.grpc.core.ContextKeys; -import io.helidon.grpc.core.GrpcTracingContext; -import io.helidon.grpc.core.GrpcTracingName; -import io.helidon.grpc.core.InterceptorWeights; -import io.helidon.tracing.HeaderConsumer; -import io.helidon.tracing.HeaderProvider; -import io.helidon.tracing.Span; -import io.helidon.tracing.Tracer; - -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ClientInterceptors; -import io.grpc.ForwardingClientCall; -import io.grpc.ForwardingClientCallListener; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.Status; - -/** - * A {@link ClientInterceptor} that captures tracing information into - * Tracing {@link Span}s for client calls. - */ -@Weight(InterceptorWeights.TRACING) -public class ClientTracingInterceptor implements ClientInterceptor { - - private final Tracer tracer; - - private final GrpcTracingName operationNameConstructor; - - private final boolean streaming; - - private final boolean verbose; - - private final Set tracedAttributes; - - /** - * Private constructor called by {@link Builder}. - * - * @param tracer the Open Tracing {@link Tracer} - * @param operationNameConstructor the operation name constructor - * @param streaming flag indicating whether to trace streaming calls - * @param verbose flag to indicate verbose logging to spans - * @param tracedAttributes the set of request attributes to add to the span - */ - private ClientTracingInterceptor(Tracer tracer, - GrpcTracingName operationNameConstructor, - boolean streaming, - boolean verbose, - Set tracedAttributes) { - this.tracer = tracer; - this.operationNameConstructor = operationNameConstructor; - this.streaming = streaming; - this.verbose = verbose; - this.tracedAttributes = tracedAttributes; - } - - /** - * Obtain a builder to build a {@link ClientTracingInterceptor}. - * - * @param tracer the {@link Tracer} to use - * @return a builder to build a {@link ClientTracingInterceptor} - */ - public static Builder builder(Tracer tracer) { - return new Builder(tracer); - } - - /** - * Use this interceptor to trace all requests made by this client channel. - * - * @param channel to be traced - * @return intercepted channel - */ - public Channel intercept(Channel channel) { - return ClientInterceptors.intercept(channel, this); - } - - @Override - public ClientCall interceptCall(MethodDescriptor method, - CallOptions callOptions, - Channel next) { - - String operationName = operationNameConstructor.name(method); - Span span = createSpanFromParent(operationName); - - for (ClientRequestAttribute attr : tracedAttributes) { - switch (attr) { - case ALL_CALL_OPTIONS: - span.tag("grpc.call_options", callOptions.toString()); - break; - case AUTHORITY: - if (callOptions.getAuthority() == null) { - span.tag("grpc.authority", "null"); - } else { - span.tag("grpc.authority", callOptions.getAuthority()); - } - break; - case COMPRESSOR: - if (callOptions.getCompressor() == null) { - span.tag("grpc.compressor", "null"); - } else { - span.tag("grpc.compressor", callOptions.getCompressor()); - } - break; - case DEADLINE: - if (callOptions.getDeadline() == null) { - span.tag("grpc.deadline_millis", "null"); - } else { - span.tag("grpc.deadline_millis", callOptions.getDeadline().timeRemaining(TimeUnit.MILLISECONDS)); - } - break; - case METHOD_NAME: - span.tag("grpc.method_name", method.getFullMethodName()); - break; - case METHOD_TYPE: - if (method.getType() == null) { - span.tag("grpc.method_type", "null"); - } else { - span.tag("grpc.method_type", method.getType().toString()); - } - break; - case HEADERS: - break; - default: - // should not happen, but can be ignored - } - } - - return new ClientTracingListener<>(next.newCall(method, callOptions), span); - } - - private Span createSpanFromParent(String operationName) { - return tracer.spanBuilder(operationName) - .update(it -> GrpcTracingContext.activeSpan().map(Span::context).ifPresent(it::parent)) - .start(); - } - - /** - * Builds the configuration of a ClientTracingInterceptor. - */ - public static class Builder { - - private final Tracer tracer; - - private GrpcTracingName operationNameConstructor; - - private boolean streaming; - - private boolean verbose; - - private Set tracedAttributes; - - /** - * Constructs a builder from a {@link Tracer}. - * - * @param tracer to use for this interceptor - * Creates a Builder with default configuration - */ - public Builder(Tracer tracer) { - this.tracer = tracer; - operationNameConstructor = MethodDescriptor::getFullMethodName; - streaming = false; - verbose = false; - tracedAttributes = new HashSet<>(); - } - - /** - * Sets the operation name. - * - * @param operationNameConstructor to name all spans created by this interceptor - * @return this Builder with configured operation name - */ - public Builder withOperationName(GrpcTracingName operationNameConstructor) { - this.operationNameConstructor = operationNameConstructor; - return this; - } - - /** - * Logs streaming events to client spans. - * - * @return this Builder configured to log streaming events - */ - public Builder withStreaming() { - streaming = true; - return this; - } - - /** - * Sets one or more attributes. - * - * @param tracedAttributes to set as tags on client spans - * created by this interceptor - * @return this Builder configured to trace attributes - */ - public Builder withTracedAttributes(ClientRequestAttribute... tracedAttributes) { - this.tracedAttributes = new HashSet<>(Arrays.asList(tracedAttributes)); - return this; - } - - /** - * Logs all request life-cycle events to client spans. - * - * @return this Builder configured to be verbose - */ - public Builder withVerbosity() { - verbose = true; - return this; - } - - /** - * Finishes building the interceptor. - * - * @return a ClientTracingInterceptor with this Builder's configuration - */ - public ClientTracingInterceptor build() { - return new ClientTracingInterceptor(tracer, - operationNameConstructor, - streaming, - verbose, - tracedAttributes); - } - } - - /** - * A {@link SimpleForwardingClientCall} that adds information - * to a tracing {@link Span} at different places in the gROC call lifecycle. - * - * @param the gRPC request type - * @param the gRPC response type - */ - private class ClientTracingListener - extends ForwardingClientCall.SimpleForwardingClientCall { - - private final Span span; - - private ClientTracingListener(ClientCall delegate, Span span) { - super(delegate); - this.span = span; - } - - @Override - public void start(Listener responseListener, final Metadata headers) { - if (verbose) { - span.addEvent("Started call"); - } - - if (tracedAttributes.contains(ClientRequestAttribute.HEADERS)) { - // copy the headers and make sure that the AUTHORIZATION header - // is removed as we do not want auth details to appear in tracing logs - Metadata metadata = new Metadata(); - metadata.merge(headers); - metadata.removeAll(ContextKeys.AUTHORIZATION); - span.tag("grpc.headers", metadata.toString()); - } - - tracer.inject(span.context(), HeaderProvider.empty(), new MetadataHeaderConsumer(headers)); - - Listener tracingResponseListener - = new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - if (verbose) { - span.addEvent("headers", - Map.of("Response headers received", headers.toString())); - } - delegate().onHeaders(headers); - } - - @Override - public void onClose(Status status, Metadata trailers) { - if (verbose) { - if (status.getCode().value() == 0) { - span.addEvent("Call closed"); - } else { - String desc = String.valueOf(status.getDescription()); - - span.addEvent("onClose", Map.of("Call failed", desc)); - } - } - span.end(); - - delegate().onClose(status, trailers); - } - - @Override - public void onMessage(RespT message) { - if (streaming || verbose) { - span.addEvent("Response received"); - } - delegate().onMessage(message); - } - }; - - delegate().start(tracingResponseListener, headers); - } - - @Override - public void sendMessage(ReqT message) { - if (streaming || verbose) { - span.addEvent("Message sent"); - } - - delegate().sendMessage(message); - } - - @Override - public void cancel(String message, Throwable cause) { - String errorMessage; - - errorMessage = message == null ? "Error" : message; - - if (cause == null) { - span.addEvent(errorMessage); - } else { - span.addEvent("error", Map.of(errorMessage, cause.getMessage())); - } - - delegate().cancel(message, cause); - } - - @Override - public void halfClose() { - if (streaming) { - span.addEvent("Finished sending messages"); - } - - delegate().halfClose(); - } - - private class MetadataHeaderConsumer implements HeaderConsumer { - private final Metadata headers; - - private MetadataHeaderConsumer(Metadata headers) { - this.headers = headers; - } - - @Override - public void setIfAbsent(String key, String... values) { - Metadata.Key headerKey = key(key); - if (!headers.containsKey(headerKey)) { - headers.put(headerKey, values[0]); - } - } - - @Override - public void set(String key, String... values) { - Metadata.Key headerKey = key(key); - headers.put(headerKey, values[0]); - } - - @Override - public Iterable keys() { - return headers.keys(); - } - - @Override - public Optional get(String key) { - return Optional.ofNullable(headers.get(key(key))); - } - - @Override - public Iterable getAll(String key) { - // map single value to list or get an empty list - return get(key).map(List::of).orElseGet(List::of); - } - - @Override - public boolean contains(String key) { - return headers.containsKey(key(key)); - } - - private Metadata.Key key(String name) { - return Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER); - } - - private void put(Metadata.Key key, String value) { - headers.put(key, value); - } - } - } -} From 5f066da34204d60cb62a530481afea094925a474 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Jul 2024 13:43:12 -0400 Subject: [PATCH 04/12] Cleans up annotation targets in API. Signed-off-by: Santiago Pericas-Geertsen --- .../main/java/io/helidon/grpc/api/Grpc.java | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java b/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java index 2bbfd89f802..4f5a50c2e70 100644 --- a/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java +++ b/grpc/api/src/main/java/io/helidon/grpc/api/Grpc.java @@ -25,10 +25,6 @@ import io.grpc.MethodDescriptor; -import static java.lang.annotation.ElementType.ANNOTATION_TYPE; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - /** * The Helidon gRPC API. */ @@ -163,8 +159,8 @@ public interface Grpc { * It is required when a {@linkplain io.helidon.grpc.api.Grpc.GrpcInterceptorBinding interceptor binding} * is used.

*/ - @Retention(RUNTIME) - @Target(TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) @Documented @interface GrpcInterceptor { } @@ -173,8 +169,8 @@ public interface Grpc { * Specifies that an annotation type is a gRPC interceptor binding type. A gRPC Interceptor binding is * used to specify the binding of a gRPC client or server interceptor to target gRPC service and methods. */ - @Target(ANNOTATION_TYPE) - @Retention(RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @Retention(RetentionPolicy.RUNTIME) @Documented @interface GrpcInterceptorBinding { } @@ -198,8 +194,8 @@ public interface Grpc { * public void updateOrder(Order order) { ... } * */ - @Target({TYPE, ElementType.METHOD}) - @Retention(RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) @interface GrpcInterceptors { /** * An ordered list of interceptors. @@ -213,8 +209,8 @@ public interface Grpc { * An annotation used to annotate a type or method to specify the * named marshaller supplier to use for rpc method calls. */ - @Target({TYPE, ElementType.METHOD}) - @Retention(RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface GrpcMarshaller { @@ -240,8 +236,8 @@ public interface Grpc { * An annotation to mark a class as representing a gRPC service * or a method as a gRPC service method. */ - @Target({ElementType.METHOD, ANNOTATION_TYPE}) - @Retention(RUNTIME) + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface GrpcMethod { @@ -265,8 +261,8 @@ public interface Grpc { /** * An annotation to indicate the request type of gRPC method. */ - @Target({ElementType.METHOD}) - @Retention(RUNTIME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface RequestType { @@ -281,8 +277,8 @@ public interface Grpc { /** * An annotation to indicate the response type of a gRPC method. */ - @Target({ElementType.METHOD}) - @Retention(RUNTIME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @interface ResponseType { @@ -297,8 +293,8 @@ public interface Grpc { /** * An annotation that can be used to specify the name of a configured gRPC channel. */ - @Target({TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PARAMETER}) - @Retention(RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) @interface GrpcChannel { /** @@ -313,7 +309,7 @@ public interface Grpc { * An annotation used to mark an injection point for a gRPC service client proxy. */ @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) - @Retention(RUNTIME) + @Retention(RetentionPolicy.RUNTIME) @interface GrpcProxy { } } From 5e8e163edc7c009b0b4b7ec02326b6dc55a609fe Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Jul 2024 17:00:22 -0400 Subject: [PATCH 05/12] Updates BOM to include gRPC client dependency. --- all/pom.xml | 4 ++++ bom/pom.xml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/all/pom.xml b/all/pom.xml index 8a423d9567e..2ea3cf4682b 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -299,6 +299,10 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-server + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-client + io.helidon.microprofile.grpc helidon-microprofile-grpc-tracing diff --git a/bom/pom.xml b/bom/pom.xml index b5532308d1a..d6b6d76825a 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -115,6 +115,11 @@ helidon-microprofile-grpc-server ${helidon.version} + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-client + ${helidon.version} + io.helidon.microprofile.grpc helidon-microprofile-grpc-tracing From 3017d206f66e8ebedc3ea0e58971d6402365dd4e Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 24 Jul 2024 11:12:41 -0400 Subject: [PATCH 06/12] Adds an gRPC MP sample application under examples/microprofile. Signed-off-by: Santiago Pericas-Geertsen --- examples/microprofile/grpc/README.md | 15 +++ examples/microprofile/grpc/pom.xml | 114 ++++++++++++++++ .../microprofile/grpc/StringService.java | 62 +++++++++ .../grpc/StringServiceClient.java | 39 ++++++ .../microprofile/grpc/package-info.java | 16 +++ .../grpc/src/main/proto/strings.proto | 30 +++++ .../src/main/resources/META-INF/beans.xml | 25 ++++ .../grpc/src/main/resources/application.yaml | 47 +++++++ .../grpc/src/main/resources/client.p12 | Bin 0 -> 4181 bytes .../src/main/resources/logging.properties | 29 +++++ .../grpc/src/main/resources/server.p12 | Bin 0 -> 4133 bytes .../microprofile/grpc/StringServiceTest.java | 122 ++++++++++++++++++ 12 files changed, 499 insertions(+) create mode 100644 examples/microprofile/grpc/README.md create mode 100644 examples/microprofile/grpc/pom.xml create mode 100644 examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java create mode 100644 examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java create mode 100644 examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java create mode 100644 examples/microprofile/grpc/src/main/proto/strings.proto create mode 100644 examples/microprofile/grpc/src/main/resources/META-INF/beans.xml create mode 100644 examples/microprofile/grpc/src/main/resources/application.yaml create mode 100644 examples/microprofile/grpc/src/main/resources/client.p12 create mode 100644 examples/microprofile/grpc/src/main/resources/logging.properties create mode 100644 examples/microprofile/grpc/src/main/resources/server.p12 create mode 100644 examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java diff --git a/examples/microprofile/grpc/README.md b/examples/microprofile/grpc/README.md new file mode 100644 index 00000000000..4fa6c590664 --- /dev/null +++ b/examples/microprofile/grpc/README.md @@ -0,0 +1,15 @@ +# Helidon gRPC MP Example + +This examples shows a simple application written using Helidon gRPC MP API: + +- StringService: a gRPC service implementation that uses MP +- StringServiceClient: an interface from which a client proxy can be created to call StringService remote methods +- StringServiceTest: a sample test that starts a server and tests the client and server components +- application.yaml: configuration for server and client channels + +## Build and run + +```shell +mvn package +java -jar target/helidon-examples-microprofile-grpc.jar +``` \ No newline at end of file diff --git a/examples/microprofile/grpc/pom.xml b/examples/microprofile/grpc/pom.xml new file mode 100644 index 00000000000..5f0c47e3ce8 --- /dev/null +++ b/examples/microprofile/grpc/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.1.0-SNAPSHOT + ../../../applications/mp/pom.xml + + io.helidon.examples.microprofile + helidon-examples-microprofile-grpc + Helidon Examples Microprofile gRPC + + + Microprofile example that uses gRPC + + + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-core + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-server + + + io.helidon.microprofile.grpc + helidon-microprofile-grpc-client + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + test + + + io.helidon.logging + helidon-logging-jul + test + + + + javax.annotation + javax.annotation-api + provided + true + + + + + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + compile + test-compile + + + + + + + \ No newline at end of file diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java new file mode 100644 index 00000000000..fa6bb944a6d --- /dev/null +++ b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java @@ -0,0 +1,62 @@ +/* + * 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.examples.microprofile.grpc; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.grpc.api.Grpc; +import io.helidon.grpc.core.CollectingObserver; +import io.helidon.grpc.core.ResponseHelper; + +import io.grpc.stub.StreamObserver; +import jakarta.enterprise.context.ApplicationScoped; + +@Grpc.GrpcService +@ApplicationScoped +public class StringService { + + @Grpc.Unary("Upper") + public Strings.StringMessage upper(Strings.StringMessage request) { + return newMessage(request.getText().toUpperCase()); + } + + @Grpc.Unary("Lower") + public Strings.StringMessage lower(Strings.StringMessage request) { + return newMessage(request.getText().toLowerCase()); + } + + @Grpc.ServerStreaming("Split") + public void split(Strings.StringMessage request, StreamObserver observer) { + String[] parts = request.getText().split(" "); + ResponseHelper.stream(observer, Stream.of(parts).map(this::newMessage)); + } + + @Grpc.ClientStreaming("Join") + public StreamObserver join(StreamObserver observer) { + return CollectingObserver.create( + Collectors.joining(" "), + observer, + Strings.StringMessage::getText, + this::newMessage); + } + + private Strings.StringMessage newMessage(String text) { + return Strings.StringMessage.newBuilder().setText(text).build(); + } +} + diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java new file mode 100644 index 00000000000..30994959de8 --- /dev/null +++ b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java @@ -0,0 +1,39 @@ +/* + * 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.examples.microprofile.grpc; + +import io.helidon.grpc.api.Grpc; + +import io.grpc.stub.StreamObserver; + +@Grpc.GrpcService("StringService") +@Grpc.GrpcChannel("string-channel") // see application.yaml +public interface StringServiceClient { + + @Grpc.Unary("Upper") + Strings.StringMessage upper(Strings.StringMessage request); + + @Grpc.Unary("Lower") + Strings.StringMessage lower(Strings.StringMessage request); + + @Grpc.ServerStreaming("Split") + void split(Strings.StringMessage request, StreamObserver observer); + + @Grpc.ClientStreaming("Join") + StreamObserver join(StreamObserver observer); +} + diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java new file mode 100644 index 00000000000..870531c0bfe --- /dev/null +++ b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java @@ -0,0 +1,16 @@ +/* + * 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.examples.microprofile.grpc; \ No newline at end of file diff --git a/examples/microprofile/grpc/src/main/proto/strings.proto b/examples/microprofile/grpc/src/main/proto/strings.proto new file mode 100644 index 00000000000..48c8bd30de2 --- /dev/null +++ b/examples/microprofile/grpc/src/main/proto/strings.proto @@ -0,0 +1,30 @@ +/* + * 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. + */ + + +syntax = "proto3"; +option java_package = "io.helidon.examples.microprofile.grpc"; + +service StringService { + rpc Upper (StringMessage) returns (StringMessage) {} + rpc Lower (StringMessage) returns (StringMessage) {} + rpc Split (StringMessage) returns (stream StringMessage) {} + rpc Join (stream StringMessage) returns (StringMessage) {} +} + +message StringMessage { + string text = 1; +} diff --git a/examples/microprofile/grpc/src/main/resources/META-INF/beans.xml b/examples/microprofile/grpc/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..69f490f1afc --- /dev/null +++ b/examples/microprofile/grpc/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/microprofile/grpc/src/main/resources/application.yaml b/examples/microprofile/grpc/src/main/resources/application.yaml new file mode 100644 index 00000000000..6dff1ae28f4 --- /dev/null +++ b/examples/microprofile/grpc/src/main/resources/application.yaml @@ -0,0 +1,47 @@ +# +# 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. +# + +server: + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "server.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "server.p12" + +grpc: + channels: + string-channel: + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "client.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "client.p12" \ No newline at end of file diff --git a/examples/microprofile/grpc/src/main/resources/client.p12 b/examples/microprofile/grpc/src/main/resources/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..4eb3b8325cd0190163032aca86e1d4fc94856822 GIT binary patch literal 4181 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 U@+;F6i_J(6+nRj}N+2@-U-0Veh5!Hn literal 0 HcmV?d00001 diff --git a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java new file mode 100644 index 00000000000..763dfc91006 --- /dev/null +++ b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java @@ -0,0 +1,122 @@ +/* + * 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.examples.microprofile.grpc; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.helidon.grpc.api.Grpc; +import io.helidon.microprofile.grpc.client.GrpcClientCdiExtension; +import io.helidon.microprofile.grpc.server.GrpcMpCdiExtension; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import io.grpc.stub.StreamObserver; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; + +@HelidonTest +@AddExtension(GrpcMpCdiExtension.class) +@AddExtension(GrpcClientCdiExtension.class) +class StringServiceTest { + + @Inject + @Grpc.GrpcProxy + private StringServiceClient client; + + @Test + void testUnaryUpper() { + Strings.StringMessage res = client.upper(newMessage("hello")); + assertThat(res.getText(), is("HELLO")); + } + + @Test + void testUnaryLower() { + Strings.StringMessage res = client.lower(newMessage("HELLO")); + assertThat(res.getText(), is("hello")); + } + + @Test + void testServerStreamingSplit() throws InterruptedException { + ListObserver response = new ListObserver<>(); + client.split(newMessage("hello world"), response); + List value = response.value(); + assertThat(value, hasSize(2)); + assertThat(value, contains(newMessage("hello"), newMessage("world"))); + } + + @Test + void testClientStreamingJoin() throws InterruptedException { + ListObserver response = new ListObserver<>(); + StreamObserver request = client.join(response); + request.onNext(newMessage("hello")); + request.onNext(newMessage("world")); + request.onCompleted(); + List value = response.value(); + assertThat(value.getFirst(), is(newMessage("hello world"))); + } + + /** + * Helper method to create a string message from a string. + * + * @param data the string + * @return the string message + */ + Strings.StringMessage newMessage(String data) { + return Strings.StringMessage.newBuilder().setText(data).build(); + } + + /** + * Helper class to collect a list of observed values. + * + * @param the type of values + */ + static class ListObserver implements StreamObserver { + private static final long TIMEOUT_SECONDS = 10; + + private List value = new ArrayList<>(); + private final CountDownLatch latch = new CountDownLatch(1); + + public List value() throws InterruptedException { + boolean b = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assert b; + return value; + } + + @Override + public void onNext(T value) { + this.value.add(value); + } + + @Override + public void onError(Throwable t) { + value = null; + } + + @Override + public void onCompleted() { + latch.countDown(); + } + } +} + From 4a22547cce81ecd63e2e964177dd78fdfa8e53f3 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 24 Jul 2024 14:00:08 -0400 Subject: [PATCH 07/12] Updates signature of split method to return a Stream. Signed-off-by: Santiago Pericas-Geertsen --- .../microprofile/grpc/StringService.java | 32 +++++++++++++++++-- .../grpc/StringServiceClient.java | 31 +++++++++++++++++- .../microprofile/grpc/package-info.java | 2 +- .../microprofile/grpc/StringServiceTest.java | 8 ++--- 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java index fa6bb944a6d..046793bd5cd 100644 --- a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java +++ b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java @@ -21,31 +21,57 @@ import io.helidon.grpc.api.Grpc; import io.helidon.grpc.core.CollectingObserver; -import io.helidon.grpc.core.ResponseHelper; import io.grpc.stub.StreamObserver; import jakarta.enterprise.context.ApplicationScoped; +/** + * An implementation of a string service. + */ @Grpc.GrpcService @ApplicationScoped public class StringService { + /** + * Uppercase a string. + * + * @param request string message + * @return string message + */ @Grpc.Unary("Upper") public Strings.StringMessage upper(Strings.StringMessage request) { return newMessage(request.getText().toUpperCase()); } + /** + * Lowercase a string. + * + * @param request string message + * @return string message + */ @Grpc.Unary("Lower") public Strings.StringMessage lower(Strings.StringMessage request) { return newMessage(request.getText().toLowerCase()); } + /** + * Split a string using space delimiters. + * + * @param request string message + * @return stream of string messages + */ @Grpc.ServerStreaming("Split") - public void split(Strings.StringMessage request, StreamObserver observer) { + public Stream split(Strings.StringMessage request) { String[] parts = request.getText().split(" "); - ResponseHelper.stream(observer, Stream.of(parts).map(this::newMessage)); + return Stream.of(parts).map(this::newMessage); } + /** + * Join a stream of messages using spaces. + * + * @param observer stream of messages + * @return single message as a stream + */ @Grpc.ClientStreaming("Join") public StreamObserver join(StreamObserver observer) { return CollectingObserver.create( diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java index 30994959de8..b03ee3e9089 100644 --- a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java +++ b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java @@ -16,23 +16,52 @@ package io.helidon.examples.microprofile.grpc; +import java.util.stream.Stream; + import io.helidon.grpc.api.Grpc; import io.grpc.stub.StreamObserver; +/** + * A client for a {@link StringService}. + */ @Grpc.GrpcService("StringService") @Grpc.GrpcChannel("string-channel") // see application.yaml public interface StringServiceClient { + /** + * Uppercase a string. + * + * @param request string message + * @return string message + */ @Grpc.Unary("Upper") Strings.StringMessage upper(Strings.StringMessage request); + /** + * Lowercase a string. + * + * @param request string message + * @return string message + */ @Grpc.Unary("Lower") Strings.StringMessage lower(Strings.StringMessage request); + /** + * Split a string using space delimiters. + * + * @param request string message + * @return stream of string messages + */ @Grpc.ServerStreaming("Split") - void split(Strings.StringMessage request, StreamObserver observer); + Stream split(Strings.StringMessage request); + /** + * Join a stream of messages using spaces. + * + * @param observer stream of messages + * @return single message as a stream + */ @Grpc.ClientStreaming("Join") StreamObserver join(StreamObserver observer); } diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java index 870531c0bfe..fbf691b8081 100644 --- a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java +++ b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.examples.microprofile.grpc; \ No newline at end of file +package io.helidon.examples.microprofile.grpc; diff --git a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java index 763dfc91006..15f51306ff4 100644 --- a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java +++ b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import io.helidon.grpc.api.Grpc; import io.helidon.microprofile.grpc.client.GrpcClientCdiExtension; @@ -57,10 +58,9 @@ void testUnaryLower() { } @Test - void testServerStreamingSplit() throws InterruptedException { - ListObserver response = new ListObserver<>(); - client.split(newMessage("hello world"), response); - List value = response.value(); + void testServerStreamingSplit() { + Stream stream = client.split(newMessage("hello world")); + List value = stream.toList(); assertThat(value, hasSize(2)); assertThat(value, contains(newMessage("hello"), newMessage("world"))); } From d868b7a78b18b3551811b5230f741208a02edf37 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 31 Jul 2024 10:52:43 -0400 Subject: [PATCH 08/12] Fixes problems in some pom files. Simplifies test. Signed-off-by: Santiago Pericas-Geertsen --- examples/microprofile/grpc/pom.xml | 2 +- .../microprofile/grpc/StringServiceTest.java | 5 ---- microprofile/grpc/client/pom.xml | 30 +++++++++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/examples/microprofile/grpc/pom.xml b/examples/microprofile/grpc/pom.xml index 5f0c47e3ce8..eadf8814af5 100644 --- a/examples/microprofile/grpc/pom.xml +++ b/examples/microprofile/grpc/pom.xml @@ -64,7 +64,7 @@ io.helidon.logging helidon-logging-jul - test + runtime javax.annotation javax.annotation-api - provided true @@ -136,13 +135,40 @@ maven-compiler-plugin + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + io.helidon.builder - helidon-builder-processor + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + org.xolstice.maven.plugins From 01be9d41475d420dd0d66ceac47756cef3b3c420 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 31 Jul 2024 14:08:52 -0400 Subject: [PATCH 09/12] Updates gRPC client channel config structure to use blueprints. Channels are now defined under grpc.client.channels. Signed-off-by: Santiago Pericas-Geertsen --- .../grpc/src/main/resources/application.yaml | 31 ++++++++-------- .../grpc/client/ChannelProducer.java | 2 +- .../GrpcChannelDescriptorBlueprint.java | 8 +++++ .../GrpcChannelsDescriptorBlueprint.java | 35 +++++++++++++++++++ .../grpc/client/GrpcChannelsProvider.java | 14 +++----- .../src/test/resources/application.yaml | 31 ++++++++-------- 6 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java diff --git a/examples/microprofile/grpc/src/main/resources/application.yaml b/examples/microprofile/grpc/src/main/resources/application.yaml index 6dff1ae28f4..fdc488cfd16 100644 --- a/examples/microprofile/grpc/src/main/resources/application.yaml +++ b/examples/microprofile/grpc/src/main/resources/application.yaml @@ -30,18 +30,19 @@ server: resource-path: "server.p12" grpc: - channels: - string-channel: - port: 0 - tls: - trust: - keystore: - passphrase: "password" - trust-store: true - resource: - resource-path: "client.p12" - private-key: - keystore: - passphrase: "password" - resource: - resource-path: "client.p12" \ No newline at end of file + client: + channels: + - name: "string-channel" + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "client.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "client.p12" \ No newline at end of file diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java index c29525e06b4..9a1cb1f3c5f 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/ChannelProducer.java @@ -47,7 +47,7 @@ public class ChannelProducer { */ @Inject ChannelProducer(Config config) { - provider = GrpcChannelsProvider.create(config.get("grpc")); + provider = GrpcChannelsProvider.create(config.get("grpc.client")); } /** diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java index 74d77fcadf1..54e7b8da1a9 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelDescriptorBlueprint.java @@ -26,6 +26,14 @@ @Prototype.Configured interface GrpcChannelDescriptorBlueprint { + /** + * The name of this channel. + * + * @return channel name + */ + @Option.Configured + String name(); + /** * The host to connect to. * diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java new file mode 100644 index 00000000000..c6932921197 --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsDescriptorBlueprint.java @@ -0,0 +1,35 @@ +/* + * 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.microprofile.grpc.client; + +import java.util.List; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; + +@Prototype.Blueprint +@Prototype.Configured +interface GrpcChannelsDescriptorBlueprint { + + /** + * List of channel descriptors. + * + * @return channel descriptors + */ + @Option.Configured + List channels(); +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java index 0ad6c5aeb3b..85fd849bd54 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java @@ -119,7 +119,7 @@ public Channel channel(String name) { throw new IllegalArgumentException("name cannot be empty or blank."); } GrpcChannelDescriptor chCfg = channelConfigs.computeIfAbsent(name, hostName -> - GrpcChannelDescriptor.builder().host(name).build()); + GrpcChannelDescriptor.builder().name(name).host(name).build()); return createChannel(name, chCfg); } @@ -176,20 +176,14 @@ public static class Builder implements io.helidon.common.Builder channelConfigs.put(ch.name(), ch)); } /** diff --git a/microprofile/grpc/client/src/test/resources/application.yaml b/microprofile/grpc/client/src/test/resources/application.yaml index bf73807e8ae..525f7570c65 100644 --- a/microprofile/grpc/client/src/test/resources/application.yaml +++ b/microprofile/grpc/client/src/test/resources/application.yaml @@ -30,18 +30,19 @@ server: resource-path: "server.p12" grpc: - channels: - echo-channel: - port: 0 - tls: - trust: - keystore: - passphrase: "password" - trust-store: true - resource: - resource-path: "client.p12" - private-key: - keystore: - passphrase: "password" - resource: - resource-path: "client.p12" \ No newline at end of file + client: + channels: + - name: "echo-channel" + port: 0 + tls: + trust: + keystore: + passphrase: "password" + trust-store: true + resource: + resource-path: "client.p12" + private-key: + keystore: + passphrase: "password" + resource: + resource-path: "client.p12" \ No newline at end of file From 0deaad77e5841f79954fb85f4bc2231563edbb07 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 1 Aug 2024 11:27:37 -0400 Subject: [PATCH 10/12] Introduces a new interface implemented by all gRPC client proxies that can be used to update the server's listening port dynamically. This is very useful during testing. Removes reflective code from the client gRPC library and updates tests. --- .../microprofile/grpc/StringServiceTest.java | 13 +++++++ .../grpc/client/GrpcChannelsProvider.java | 32 ----------------- .../grpc/client/GrpcClientBuilder.java | 2 +- .../grpc/client/GrpcConfigurablePort.java | 35 +++++++++++++++++++ .../grpc/client/GrpcProxyBuilder.java | 2 +- .../grpc/client/GrpcServiceClient.java | 12 +++++++ .../grpc/client/EchoServiceTest.java | 8 +++++ .../helidon/webclient/grpc/GrpcChannel.java | 11 +++++- .../io/helidon/webclient/grpc/GrpcClient.java | 7 ++++ .../webclient/grpc/GrpcClientImpl.java | 5 +++ 10 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java diff --git a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java index 0d37d16cde3..b6e9410f134 100644 --- a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java +++ b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java @@ -22,10 +22,13 @@ import java.util.stream.Stream; import io.helidon.grpc.api.Grpc; +import io.helidon.microprofile.grpc.client.GrpcConfigurablePort; import io.helidon.microprofile.testing.junit5.HelidonTest; import io.grpc.stub.StreamObserver; import jakarta.inject.Inject; +import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; @@ -36,10 +39,20 @@ @HelidonTest class StringServiceTest { + @Inject + private WebTarget webTarget; + @Inject @Grpc.GrpcProxy private StringServiceClient client; + @BeforeEach + void updatePort() { + if (client instanceof GrpcConfigurablePort c) { + c.channelPort(webTarget.getUri().getPort()); + } + } + @Test void testUnaryUpper() { Strings.StringMessage res = client.upper(newMessage("hello")); diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java index 85fd849bd54..aacbc1f25d6 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcChannelsProvider.java @@ -16,7 +16,6 @@ package io.helidon.microprofile.grpc.client; -import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -25,8 +24,6 @@ import io.helidon.webclient.grpc.GrpcClient; import io.grpc.Channel; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.enterprise.inject.spi.Extension; /** * GrpcChannelsProvider is a factory for pre-configured gRPC Channel instances. @@ -43,11 +40,6 @@ public class GrpcChannelsProvider { */ public static final String DEFAULT_HOST = "localhost"; - /** - * The configuration key for the channels' configuration. - */ - public static final String CFG_KEY_CHANNELS = "channels"; - /** * A constant for holding the default port (which is "1408"). */ @@ -136,9 +128,6 @@ private Channel createChannel(String name, GrpcChannelDescriptor descriptor) { throw new IllegalArgumentException("Client TLS must be configured for gRPC proxy client"); } int port = descriptor.port(); - if (port <= 0) { - port = discoverServerPort(name); - } GrpcClient grpcClient = GrpcClient.builder() .tls(clientTls) .baseUri("https://" + descriptor.host() + ":" + port) @@ -146,27 +135,6 @@ private Channel createChannel(String name, GrpcChannelDescriptor descriptor) { return grpcClient.channel(); } - /** - * Used for unit testing: if port not set, try to obtain port from server CDI - * extension via reflection, without introducing a static dependency. - * - * @param name the channel name - * @return server port - * @throws java.lang.IllegalArgumentException if unable to discover port - */ - @SuppressWarnings("unchecked") - private static int discoverServerPort(String name) { - try { - Class extClass = (Class) Class - .forName("io.helidon.microprofile.server.ServerCdiExtension"); - Extension extension = CDI.current().getBeanManager().getExtension(extClass); - Method m = extension.getClass().getMethod("port", String.class); - return (int) m.invoke(extension, new Object[] {"@default"}); - } catch (ReflectiveOperationException e) { - throw new IllegalArgumentException("Unable to determine server port for channel " + name); - } - } - /** * Builder builds an instance of {@link GrpcChannelsProvider}. */ diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java index baf4ce5ebaf..b391de0116a 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcClientBuilder.java @@ -32,7 +32,7 @@ import static java.lang.System.Logger.Level; /** - * A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} instances + * A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor} instances * from an annotated POJO. */ class GrpcClientBuilder extends AbstractServiceBuilder diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java new file mode 100644 index 00000000000..9742daf7bac --- /dev/null +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcConfigurablePort.java @@ -0,0 +1,35 @@ +/* + * 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.microprofile.grpc.client; + +/** + * Interface implemented by all gRPC client proxies. The method {@link #channelPort} can be + * called at runtime to override the client URI port from config. Typically used for testing. + */ +public interface GrpcConfigurablePort { + + /** + * Name of single setter method on this interface. + */ + String CHANNEL_PORT = "channelPort"; + + /** + * Overrides client URI port. + * + * @param value the new port value + */ + void channelPort(int value); +} diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java index a1fb6b29151..82d9bea7f00 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcProxyBuilder.java @@ -68,7 +68,7 @@ public static GrpcProxyBuilder create(Channel channel, Class type) { */ @Override public T build() { - return client.proxy(type); + return client.proxy(type, GrpcConfigurablePort.class); } private static ClientServiceDescriptor createDescriptor(Class type) { diff --git a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java index 8bb65eb4b03..022a2293a36 100644 --- a/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java +++ b/microprofile/grpc/client/src/main/java/io/helidon/microprofile/grpc/client/GrpcServiceClient.java @@ -30,6 +30,7 @@ import io.helidon.grpc.core.InterceptorWeights; import io.helidon.grpc.core.MethodHandler; import io.helidon.grpc.core.WeightedBag; +import io.helidon.webclient.api.ClientUri; import io.grpc.CallOptions; import io.grpc.Channel; @@ -45,6 +46,8 @@ */ public class GrpcServiceClient { + private final Channel channel; + private final HashMap> methodStubs; private final ClientServiceDescriptor clientServiceDescriptor; @@ -76,6 +79,7 @@ public static GrpcServiceClient create(Channel channel, ClientServiceDescriptor private GrpcServiceClient(Channel channel, CallOptions callOptions, ClientServiceDescriptor clientServiceDescriptor) { + this.channel = channel; this.clientServiceDescriptor = clientServiceDescriptor; this.methodStubs = new HashMap<>(); @@ -137,6 +141,13 @@ public String serviceName() { Object invoke(String name, Object[] args) { GrpcMethodStub stub = methodStubs.get(name); if (stub == null) { + // may be used during testing to override a channel's port + if (name.equals(GrpcConfigurablePort.CHANNEL_PORT)) { + io.helidon.webclient.grpc.GrpcChannel grpcChannel = (io.helidon.webclient.grpc.GrpcChannel) channel; + ClientUri uri = grpcChannel.grpcClient().clientConfig().baseUri().orElseThrow(); + uri.port((int) args[0]); + return null; + } throw Status.INTERNAL.withDescription("gRPC method '" + name + "' does not exist").asRuntimeException(); } ClientMethodDescriptor descriptor = stub.descriptor(); @@ -164,6 +175,7 @@ Object invoke(String name, Object[] args) { @SuppressWarnings("unchecked") public T proxy(Class type, Class... extraTypes) { Map names = new HashMap<>(); + names.put(GrpcConfigurablePort.CHANNEL_PORT, GrpcConfigurablePort.CHANNEL_PORT); // for testing for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) { MethodHandler methodHandler = methodDescriptor.methodHandler(); if (methodHandler != null) { diff --git a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java index 7d46dd392b5..68db09e9b91 100644 --- a/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java +++ b/microprofile/grpc/client/src/test/java/io/helidon/microprofile/grpc/client/EchoServiceTest.java @@ -33,6 +33,7 @@ import io.grpc.stub.StreamObserver; import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static io.helidon.grpc.core.ResponseHelper.complete; @@ -53,6 +54,13 @@ class EchoServiceTest { @Grpc.GrpcProxy private EchoServiceClient proxyClient; + @BeforeEach + void updatePort() { + if (proxyClient instanceof GrpcConfigurablePort client) { + client.channelPort(webTarget.getUri().getPort()); + } + } + @Test void testEcho() throws InterruptedException, ExecutionException, TimeoutException { Tls clientTls = Tls.builder() diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java index 3efb29e47f7..f4d5e006cb7 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java @@ -26,7 +26,7 @@ /** * Helidon's implementation of a gRPC {@link Channel}. */ -class GrpcChannel extends Channel { +public class GrpcChannel extends Channel { private final GrpcClientImpl grpcClient; @@ -39,6 +39,15 @@ class GrpcChannel extends Channel { this.grpcClient = (GrpcClientImpl) grpcClient; } + /** + * Underlying gRPC Client for this channel. + * + * @return the gRPC client + */ + public GrpcClient grpcClient() { + return grpcClient; + } + @Override public ClientCall newCall( MethodDescriptor methodDescriptor, CallOptions callOptions) { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java index d786f7840f0..b4ffdc582cf 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java @@ -114,4 +114,11 @@ static GrpcClient create() { default Channel channel(Collection interceptors) { return channel(interceptors.toArray(new ClientInterceptor[]{})); } + + /** + * Configuration for this gRPC client. + * + * @return the configuration + */ + GrpcClientConfig clientConfig(); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java index 7a8ce6dfa91..077b8e9a83f 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java @@ -61,4 +61,9 @@ public Channel channel() { public Channel channel(ClientInterceptor... interceptors) { return ClientInterceptors.intercept(channel(), interceptors); } + + @Override + public GrpcClientConfig clientConfig() { + return clientConfig; + } } From 05a22b6d6106f2272b9bdd449f3e8ad88ee2cdae Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 2 Aug 2024 13:39:02 -0400 Subject: [PATCH 11/12] Removes new example now that our examples have been moved to a separate repo in 4.x. A new PR against the examples repo will be created. Signed-off-by: Santiago Pericas-Geertsen --- examples/microprofile/grpc/README.md | 15 -- examples/microprofile/grpc/pom.xml | 114 --------------- .../microprofile/grpc/StringService.java | 88 ------------ .../grpc/StringServiceClient.java | 68 --------- .../microprofile/grpc/package-info.java | 16 --- .../grpc/src/main/proto/strings.proto | 30 ---- .../src/main/resources/META-INF/beans.xml | 25 ---- .../grpc/src/main/resources/application.yaml | 48 ------- .../grpc/src/main/resources/client.p12 | Bin 4181 -> 0 bytes .../src/main/resources/logging.properties | 29 ---- .../grpc/src/main/resources/server.p12 | Bin 4133 -> 0 bytes .../microprofile/grpc/StringServiceTest.java | 130 ------------------ 12 files changed, 563 deletions(-) delete mode 100644 examples/microprofile/grpc/README.md delete mode 100644 examples/microprofile/grpc/pom.xml delete mode 100644 examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java delete mode 100644 examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java delete mode 100644 examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java delete mode 100644 examples/microprofile/grpc/src/main/proto/strings.proto delete mode 100644 examples/microprofile/grpc/src/main/resources/META-INF/beans.xml delete mode 100644 examples/microprofile/grpc/src/main/resources/application.yaml delete mode 100644 examples/microprofile/grpc/src/main/resources/client.p12 delete mode 100644 examples/microprofile/grpc/src/main/resources/logging.properties delete mode 100644 examples/microprofile/grpc/src/main/resources/server.p12 delete mode 100644 examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java diff --git a/examples/microprofile/grpc/README.md b/examples/microprofile/grpc/README.md deleted file mode 100644 index 4fa6c590664..00000000000 --- a/examples/microprofile/grpc/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Helidon gRPC MP Example - -This examples shows a simple application written using Helidon gRPC MP API: - -- StringService: a gRPC service implementation that uses MP -- StringServiceClient: an interface from which a client proxy can be created to call StringService remote methods -- StringServiceTest: a sample test that starts a server and tests the client and server components -- application.yaml: configuration for server and client channels - -## Build and run - -```shell -mvn package -java -jar target/helidon-examples-microprofile-grpc.jar -``` \ No newline at end of file diff --git a/examples/microprofile/grpc/pom.xml b/examples/microprofile/grpc/pom.xml deleted file mode 100644 index eadf8814af5..00000000000 --- a/examples/microprofile/grpc/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - 4.0.0 - - io.helidon.applications - helidon-mp - 4.1.0-SNAPSHOT - ../../../applications/mp/pom.xml - - io.helidon.examples.microprofile - helidon-examples-microprofile-grpc - Helidon Examples Microprofile gRPC - - - Microprofile example that uses gRPC - - - - - io.helidon.grpc - helidon-grpc-core - - - io.helidon.microprofile.grpc - helidon-microprofile-grpc-core - - - io.helidon.microprofile.grpc - helidon-microprofile-grpc-server - - - io.helidon.microprofile.grpc - helidon-microprofile-grpc-client - - - io.helidon.microprofile.testing - helidon-microprofile-testing-junit5 - test - - - io.helidon.webserver.testing.junit5 - helidon-webserver-testing-junit5-grpc - test - - - io.helidon.logging - helidon-logging-jul - runtime - - - - javax.annotation - javax.annotation-api - provided - true - - - - - - - - kr.motd.maven - os-maven-plugin - ${version.plugin.os} - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-libs - - - - - org.xolstice.maven.plugins - protobuf-maven-plugin - - - - compile - test-compile - - - - - - - \ No newline at end of file diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java deleted file mode 100644 index 046793bd5cd..00000000000 --- a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringService.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.examples.microprofile.grpc; - -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import io.helidon.grpc.api.Grpc; -import io.helidon.grpc.core.CollectingObserver; - -import io.grpc.stub.StreamObserver; -import jakarta.enterprise.context.ApplicationScoped; - -/** - * An implementation of a string service. - */ -@Grpc.GrpcService -@ApplicationScoped -public class StringService { - - /** - * Uppercase a string. - * - * @param request string message - * @return string message - */ - @Grpc.Unary("Upper") - public Strings.StringMessage upper(Strings.StringMessage request) { - return newMessage(request.getText().toUpperCase()); - } - - /** - * Lowercase a string. - * - * @param request string message - * @return string message - */ - @Grpc.Unary("Lower") - public Strings.StringMessage lower(Strings.StringMessage request) { - return newMessage(request.getText().toLowerCase()); - } - - /** - * Split a string using space delimiters. - * - * @param request string message - * @return stream of string messages - */ - @Grpc.ServerStreaming("Split") - public Stream split(Strings.StringMessage request) { - String[] parts = request.getText().split(" "); - return Stream.of(parts).map(this::newMessage); - } - - /** - * Join a stream of messages using spaces. - * - * @param observer stream of messages - * @return single message as a stream - */ - @Grpc.ClientStreaming("Join") - public StreamObserver join(StreamObserver observer) { - return CollectingObserver.create( - Collectors.joining(" "), - observer, - Strings.StringMessage::getText, - this::newMessage); - } - - private Strings.StringMessage newMessage(String text) { - return Strings.StringMessage.newBuilder().setText(text).build(); - } -} - diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java deleted file mode 100644 index b03ee3e9089..00000000000 --- a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/StringServiceClient.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.examples.microprofile.grpc; - -import java.util.stream.Stream; - -import io.helidon.grpc.api.Grpc; - -import io.grpc.stub.StreamObserver; - -/** - * A client for a {@link StringService}. - */ -@Grpc.GrpcService("StringService") -@Grpc.GrpcChannel("string-channel") // see application.yaml -public interface StringServiceClient { - - /** - * Uppercase a string. - * - * @param request string message - * @return string message - */ - @Grpc.Unary("Upper") - Strings.StringMessage upper(Strings.StringMessage request); - - /** - * Lowercase a string. - * - * @param request string message - * @return string message - */ - @Grpc.Unary("Lower") - Strings.StringMessage lower(Strings.StringMessage request); - - /** - * Split a string using space delimiters. - * - * @param request string message - * @return stream of string messages - */ - @Grpc.ServerStreaming("Split") - Stream split(Strings.StringMessage request); - - /** - * Join a stream of messages using spaces. - * - * @param observer stream of messages - * @return single message as a stream - */ - @Grpc.ClientStreaming("Join") - StreamObserver join(StreamObserver observer); -} - diff --git a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java b/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java deleted file mode 100644 index fbf691b8081..00000000000 --- a/examples/microprofile/grpc/src/main/java/io/helidon/examples/microprofile/grpc/package-info.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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.examples.microprofile.grpc; diff --git a/examples/microprofile/grpc/src/main/proto/strings.proto b/examples/microprofile/grpc/src/main/proto/strings.proto deleted file mode 100644 index 48c8bd30de2..00000000000 --- a/examples/microprofile/grpc/src/main/proto/strings.proto +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - - -syntax = "proto3"; -option java_package = "io.helidon.examples.microprofile.grpc"; - -service StringService { - rpc Upper (StringMessage) returns (StringMessage) {} - rpc Lower (StringMessage) returns (StringMessage) {} - rpc Split (StringMessage) returns (stream StringMessage) {} - rpc Join (stream StringMessage) returns (StringMessage) {} -} - -message StringMessage { - string text = 1; -} diff --git a/examples/microprofile/grpc/src/main/resources/META-INF/beans.xml b/examples/microprofile/grpc/src/main/resources/META-INF/beans.xml deleted file mode 100644 index 69f490f1afc..00000000000 --- a/examples/microprofile/grpc/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/examples/microprofile/grpc/src/main/resources/application.yaml b/examples/microprofile/grpc/src/main/resources/application.yaml deleted file mode 100644 index fdc488cfd16..00000000000 --- a/examples/microprofile/grpc/src/main/resources/application.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# -# 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. -# - -server: - port: 0 - tls: - trust: - keystore: - passphrase: "password" - trust-store: true - resource: - resource-path: "server.p12" - private-key: - keystore: - passphrase: "password" - resource: - resource-path: "server.p12" - -grpc: - client: - channels: - - name: "string-channel" - port: 0 - tls: - trust: - keystore: - passphrase: "password" - trust-store: true - resource: - resource-path: "client.p12" - private-key: - keystore: - passphrase: "password" - resource: - resource-path: "client.p12" \ No newline at end of file diff --git a/examples/microprofile/grpc/src/main/resources/client.p12 b/examples/microprofile/grpc/src/main/resources/client.p12 deleted file mode 100644 index 4eb3b8325cd0190163032aca86e1d4fc94856822..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4181 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 U@+;F6i_J(6+nRj}N+2@-U-0Veh5!Hn diff --git a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java b/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java deleted file mode 100644 index b6e9410f134..00000000000 --- a/examples/microprofile/grpc/src/test/java/io/helidon/examples/microprofile/grpc/StringServiceTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.examples.microprofile.grpc; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -import io.helidon.grpc.api.Grpc; -import io.helidon.microprofile.grpc.client.GrpcConfigurablePort; -import io.helidon.microprofile.testing.junit5.HelidonTest; - -import io.grpc.stub.StreamObserver; -import jakarta.inject.Inject; -import jakarta.ws.rs.client.WebTarget; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasSize; - -@HelidonTest -class StringServiceTest { - - @Inject - private WebTarget webTarget; - - @Inject - @Grpc.GrpcProxy - private StringServiceClient client; - - @BeforeEach - void updatePort() { - if (client instanceof GrpcConfigurablePort c) { - c.channelPort(webTarget.getUri().getPort()); - } - } - - @Test - void testUnaryUpper() { - Strings.StringMessage res = client.upper(newMessage("hello")); - assertThat(res.getText(), is("HELLO")); - } - - @Test - void testUnaryLower() { - Strings.StringMessage res = client.lower(newMessage("HELLO")); - assertThat(res.getText(), is("hello")); - } - - @Test - void testServerStreamingSplit() { - Stream stream = client.split(newMessage("hello world")); - List value = stream.toList(); - assertThat(value, hasSize(2)); - assertThat(value, contains(newMessage("hello"), newMessage("world"))); - } - - @Test - void testClientStreamingJoin() throws InterruptedException { - ListObserver response = new ListObserver<>(); - StreamObserver request = client.join(response); - request.onNext(newMessage("hello")); - request.onNext(newMessage("world")); - request.onCompleted(); - List value = response.value(); - assertThat(value.getFirst(), is(newMessage("hello world"))); - } - - /** - * Helper method to create a string message from a string. - * - * @param data the string - * @return the string message - */ - Strings.StringMessage newMessage(String data) { - return Strings.StringMessage.newBuilder().setText(data).build(); - } - - /** - * Helper class to collect a list of observed values. - * - * @param the type of values - */ - static class ListObserver implements StreamObserver { - private static final long TIMEOUT_SECONDS = 10; - - private List value = new ArrayList<>(); - private final CountDownLatch latch = new CountDownLatch(1); - - public List value() throws InterruptedException { - boolean b = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assert b; - return value; - } - - @Override - public void onNext(T value) { - this.value.add(value); - } - - @Override - public void onError(Throwable t) { - value = null; - } - - @Override - public void onCompleted() { - latch.countDown(); - } - } -} - From 6a925560397477da069b8bec38f363b691b01cc5 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 8 Aug 2024 13:55:01 -0400 Subject: [PATCH 12/12] Cleaning of pom and module-info files. Signed-off-by: Santiago Pericas-Geertsen --- microprofile/grpc/client/pom.xml | 8 -------- microprofile/grpc/client/src/main/java/module-info.java | 1 - 2 files changed, 9 deletions(-) diff --git a/microprofile/grpc/client/pom.xml b/microprofile/grpc/client/pom.xml index 653580e479b..f2b2c481fba 100644 --- a/microprofile/grpc/client/pom.xml +++ b/microprofile/grpc/client/pom.xml @@ -45,10 +45,6 @@ io.helidon.config helidon-config - - io.helidon.tracing - helidon-tracing - io.helidon.webclient helidon-webclient-grpc @@ -57,10 +53,6 @@ io.helidon.common helidon-common-tls - - io.helidon.codegen - helidon-codegen - jakarta.inject jakarta.inject-api diff --git a/microprofile/grpc/client/src/main/java/module-info.java b/microprofile/grpc/client/src/main/java/module-info.java index 02a914828d5..b4fa484d1e3 100644 --- a/microprofile/grpc/client/src/main/java/module-info.java +++ b/microprofile/grpc/client/src/main/java/module-info.java @@ -21,7 +21,6 @@ requires io.helidon.common; requires io.helidon.common.tls; - requires io.helidon.tracing; requires io.helidon.config; requires io.helidon.webclient.grpc; requires io.helidon.grpc.api;