From e275dd26edaa5f76c3c1c4d7b9f79731b7811558 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 26 Jan 2024 11:33:05 -0500 Subject: [PATCH 01/38] Initial commit based on in-progress work from Tomas. Signed-off-by: Santiago Pericasgeertsen --- all/pom.xml | 4 + bom/pom.xml | 16 + examples/webserver/protocols/pom.xml | 40 +- .../webserver/protocols/ProtocolsMain.java | 12 +- .../webserver/protocols/ProtocolsTest.java | 45 ++ grpc/core/pom.xml | 85 ++++ .../io/helidon/grpc/core/ContextKeys.java | 52 ++ .../helidon/grpc/core/GrpcTracingContext.java | 45 ++ .../io/helidon/grpc/core/GrpcTracingName.java | 32 ++ .../grpc/core/InterceptorPriorities.java | 63 +++ .../helidon/grpc/core/MarshallerSupplier.java | 105 ++++ .../io/helidon/grpc/core/MethodHandler.java | 191 ++++++++ .../io/helidon/grpc/core/PriorityBag.java | 214 ++++++++ .../io/helidon/grpc/core/ResponseHelper.java | 455 ++++++++++++++++++ .../helidon/grpc/core/SafeStreamObserver.java | 169 +++++++ .../io/helidon/grpc/core/package-info.java | 20 + grpc/core/src/main/java/module-info.java | 37 ++ grpc/pom.xml | 38 ++ pom.xml | 1 + webclient/grpc/pom.xml | 146 ++++++ .../grpc/ClientMethodDescriptor.java | 419 ++++++++++++++++ .../io/helidon/webclient/grpc/GrpcClient.java | 83 ++++ .../webclient/grpc/GrpcClientCall.java | 77 +++ .../grpc/GrpcClientConfigBlueprint.java | 37 ++ .../webclient/grpc/GrpcClientImpl.java | 42 ++ .../webclient/grpc/GrpcClientProtocol.java | 34 ++ .../GrpcClientProtocolConfigBlueprint.java | 39 ++ .../webclient/grpc/GrpcClientStream.java | 75 +++ .../grpc/GrpcProtocolConfigProvider.java | 45 ++ .../webclient/grpc/GrpcProtocolProvider.java | 53 ++ .../webclient/grpc/GrpcServiceClient.java | 51 ++ .../webclient/grpc/GrpcServiceClientImpl.java | 97 ++++ .../webclient/grpc/GrpcServiceDescriptor.java | 42 ++ webclient/grpc/src/main/java/module-info.java | 47 ++ .../helidon/webclient/http2/Http2Client.java | 6 + webclient/pom.xml | 1 + .../helidon/webclient/websocket/WsClient.java | 2 +- .../java/io/helidon/webserver/grpc/Grpc.java | 15 +- .../webserver/grpc/GrpcProtocolSelector.java | 2 +- .../helidon/webserver/grpc/GrpcRouting.java | 19 + .../webserver/grpc/GrpcServerCalls.java | 137 ++++++ .../helidon/webserver/grpc/GrpcService.java | 4 + .../webserver/grpc/GrpcServiceRoute.java | 20 + webserver/testing/junit5/grpc/pom.xml | 57 +++ .../junit5/grpc/GrpcServerExtension.java | 59 +++ .../grpc/src/main/java/module-info.java | 32 ++ webserver/testing/junit5/pom.xml | 1 + 47 files changed, 3256 insertions(+), 10 deletions(-) create mode 100644 examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java create mode 100644 grpc/core/pom.xml create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/package-info.java create mode 100644 grpc/core/src/main/java/module-info.java create mode 100644 grpc/pom.xml create mode 100644 webclient/grpc/pom.xml create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java create mode 100644 webclient/grpc/src/main/java/module-info.java create mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java create mode 100644 webserver/testing/junit5/grpc/pom.xml create mode 100644 webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java create mode 100644 webserver/testing/junit5/grpc/src/main/java/module-info.java diff --git a/all/pom.xml b/all/pom.xml index 12e171fdcd0..d949614f078 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -978,6 +978,10 @@ io.helidon.webclient helidon-webclient-websocket + + io.helidon.webclient + helidon-webclient-grpc + io.helidon.webclient helidon-webclient-sse diff --git a/bom/pom.xml b/bom/pom.xml index ffd88f9a7e4..21811b1ba92 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -94,6 +94,12 @@ helidon-microprofile-graphql-server ${helidon.version} + + + io.helidon.grpc + helidon-grpc-core + ${helidon.version} + io.helidon.integrations.micronaut @@ -1257,6 +1263,11 @@ helidon-webserver-testing-junit5-websocket ${helidon.version} + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + ${helidon.version} + io.helidon.webclient helidon-webclient-api @@ -1282,6 +1293,11 @@ helidon-webclient-websocket ${helidon.version} + + io.helidon.webclient + helidon-webclient-grpc + ${helidon.version} + io.helidon.webclient helidon-webclient-sse diff --git a/examples/webserver/protocols/pom.xml b/examples/webserver/protocols/pom.xml index 67bd3b2bd79..ff2dfc32f6e 100644 --- a/examples/webserver/protocols/pom.xml +++ b/examples/webserver/protocols/pom.xml @@ -44,21 +44,33 @@ io.helidon.webserver helidon-webserver + + io.helidon.webserver + helidon-webserver-http2 + + + io.helidon.webserver + helidon-webserver-websocket + io.helidon.webserver helidon-webserver-grpc + + io.helidon.webclient + helidon-webclient + io.helidon.webclient helidon-webclient-http2 - io.helidon.webserver - helidon-webserver-websocket + io.helidon.webclient + helidon-webclient-websocket - io.helidon.webserver - helidon-webserver-http2 + io.helidon.webclient + helidon-webclient-grpc org.junit.jupiter @@ -70,6 +82,26 @@ hamcrest-all test + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-http2 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-websocket + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + test + diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java index e8010bacc8f..0a1a9fb69c2 100644 --- a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -79,13 +79,23 @@ public static void main(String[] args) { .unary(Strings.getDescriptor(), "StringService", "Upper", - ProtocolsMain::grpcUpper)) + ProtocolsMain::grpcUpper) + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + ProtocolsMain::blockingGrpcUpper)) .addRouting(WsRouting.builder() .endpoint("/tyrus/echo", ProtocolsMain::wsEcho)) .build() .start(); } + private static Strings.StringMessage blockingGrpcUpper(Strings.StringMessage reqT) { + return Strings.StringMessage.newBuilder() + .setText(reqT.getText().toUpperCase(Locale.ROOT)) + .build(); + } + private static void grpcUpper(Strings.StringMessage request, StreamObserver observer) { String requestText = request.getText(); System.out.println("grpc request: " + requestText); diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java new file mode 100644 index 00000000000..59beccb914e --- /dev/null +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java @@ -0,0 +1,45 @@ +/* + * 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.webserver.protocols; + +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import io.helidon.webclient.websocket.WsClient; +import io.helidon.webserver.testing.junit5.ServerTest; +import org.junit.jupiter.api.Test; + +@ServerTest +class ProtocolsTest { + + private final WebClient webClient; + private final WsClient wsClient; + private final GrpcClient grpcClient; + + private ProtocolsTest(WebClient webClient, WsClient wsClient, GrpcClient grpcClient) { + this.webClient = webClient; + this.wsClient = wsClient; + this.grpcClient = grpcClient; + } + + @Test + void test() { + grpcClient.serviceClient( + GrpcServiceDescriptor + ) + } +} diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml new file mode 100644 index 00000000000..def59c84858 --- /dev/null +++ b/grpc/core/pom.xml @@ -0,0 +1,85 @@ + + + + + + helidon-grpc-project + io.helidon.grpc + 4.0.0-SNAPSHOT + + 4.0.0 + + helidon-grpc-core + Helidon gRPC related modules + + + + io.helidon.common + helidon-common-context + + + io.helidon.common + helidon-common-config + + + io.helidon.tracing + helidon-tracing + + + io.grpc + grpc-api + + + io.grpc + grpc-core + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + com.google.j2objc + j2objc-annotations + + + + + jakarta.inject + jakarta.inject-api + + + jakarta.annotation + jakarta.annotation-api + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java new file mode 100644 index 00000000000..f51358e14e4 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019, 2021 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.grpc.core; + +import java.lang.reflect.Method; + +import io.grpc.Context; +import io.grpc.Metadata; + +/** + * A collection of common gRPC {@link Context.Key} and + * {@link Metadata.Key} instances. + */ +public final class ContextKeys { + /** + * The {@link Metadata.Key} to use to obtain the authorization data. + */ + public static final Metadata.Key AUTHORIZATION = + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + + /** + * The gRPC context key to use to obtain the Helidon {@link io.helidon.common.context.Context} + * from the gRPC {@link Context}. + */ + public static final Context.Key HELIDON_CONTEXT = + Context.key(io.helidon.common.context.Context.class.getCanonicalName()); + + /** + * The {@link Context.Key} to use to obtain the actual underlying rpc {@link Method}. + */ + public static final Context.Key SERVICE_METHOD = Context.key(Method.class.getName()); + + /** + * Private constructor for utility class. + */ + private ContextKeys() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java new file mode 100644 index 00000000000..e310ed43937 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 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.grpc.core; + +import java.util.Optional; + +import io.grpc.Context; +import io.helidon.tracing.Span; + +/** + * Contextual information related to Tracing. + */ +public final class GrpcTracingContext { + private static final String SPAN_KEY_NAME = "io.helidon.tracing.active-span"; + + /** + * Context key for Span instance. + */ + public static final Context.Key SPAN_KEY = Context.key(SPAN_KEY_NAME); + + /** + * Get the current active span associated with the context. + * + * @return span if one is in current context + */ + public static Optional activeSpan() { + return Optional.ofNullable(SPAN_KEY.get()); + } + + private GrpcTracingContext() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java new file mode 100644 index 00000000000..1a48b0f2f8a --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.grpc.core; + +import io.grpc.MethodDescriptor; + +/** + * Name generator for span operation name. + */ +@FunctionalInterface +public interface GrpcTracingName { + /** + * Constructs a span's operation name from the gRPC method. + * + * @param method method to extract a name from + * @return operation name + */ + String name(MethodDescriptor method); +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java new file mode 100644 index 00000000000..d4da4008e66 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019, 2021 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.grpc.core; + +/** + * Constants that represent a priority ordering that interceptors registered with + * a gRPC service or method will be applied. + */ +public class InterceptorPriorities { + /** + * Context priority. + *

+ * Interceptors with this priority typically only perform tasks + * such as adding state to the call {@link io.grpc.Context}. + */ + public static final int CONTEXT = 1000; + + /** + * Tracing priority. + *

+ * Tracing and metrics interceptors are typically applied after any context + * interceptors so that they can trace and gather metrics on the whole call + * stack of remaining interceptors. + */ + public static final int TRACING = CONTEXT + 1; + + /** + * Security authentication priority. + */ + public static final int AUTHENTICATION = 2000; + + /** + * Security authorization priority. + */ + public static final int AUTHORIZATION = 2000; + + /** + * User-level priority. + * + * This value is also used as a default priority for application-supplied interceptors. + */ + public static final int USER = 5000; + + /** + * Cannot create instances. + */ + private InterceptorPriorities() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java new file mode 100644 index 00000000000..47e14278f76 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019, 2022 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.grpc.core; + +import com.google.protobuf.MessageLite; +import io.grpc.MethodDescriptor; +import io.grpc.protobuf.lite.ProtoLiteUtils; +import jakarta.inject.Named; + +/** + * A supplier of {@link MethodDescriptor.Marshaller} instances for specific + * classes. + */ +@FunctionalInterface +public interface MarshallerSupplier { + + /** + * The name of the Protocol Buffer marshaller supplier. + */ + String PROTO = "proto"; + + /** + * The name to use to specify the default marshaller supplier. + */ + String DEFAULT = "default"; + + /** + * Obtain a {@link MethodDescriptor.Marshaller} for a type. + * + * @param clazz the {@link Class} of the type to obtain the {@link MethodDescriptor.Marshaller} for + * @param the type to be marshalled + * + * @return a {@link MethodDescriptor.Marshaller} for a type + */ + MethodDescriptor.Marshaller get(Class clazz); + + /** + * Obtain the default marshaller. + * + * @return the default marshaller + */ + static MarshallerSupplier defaultInstance() { + return new DefaultMarshallerSupplier(); + } + + /** + * The default {@link MarshallerSupplier}. + */ + @Named(MarshallerSupplier.DEFAULT) + class DefaultMarshallerSupplier + implements MarshallerSupplier { + + private final ProtoMarshallerSupplier proto = new ProtoMarshallerSupplier(); + + @Override + public MethodDescriptor.Marshaller get(Class clazz) { + if (MessageLite.class.isAssignableFrom(clazz)) { + return proto.get(clazz); + } + String msg = String.format( + "Class %s must be a valid ProtoBuf message, or a custom marshaller for it must be specified explicitly", + clazz.getName()); + throw new IllegalArgumentException(msg); + } + } + + /** + * A {@link MarshallerSupplier} implementation that + * supplies Protocol Buffer marshaller instances. + */ + @Named(PROTO) + class ProtoMarshallerSupplier + implements MarshallerSupplier { + + @Override + @SuppressWarnings("unchecked") + public MethodDescriptor.Marshaller get(Class clazz) { + try { + java.lang.reflect.Method getDefaultInstance = clazz.getDeclaredMethod("getDefaultInstance"); + MessageLite instance = (MessageLite) getDefaultInstance.invoke(clazz); + + return (MethodDescriptor.Marshaller) ProtoLiteUtils.marshaller(instance); + } catch (Exception e) { + String msg = String.format( + "Attempting to use class %s, which is not a valid Protocol buffer message, with a default marshaller", + clazz.getName()); + throw new IllegalArgumentException(msg); + } + } + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java b/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java new file mode 100644 index 00000000000..205152293f5 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2019, 2021 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.grpc.core; + +import java.util.concurrent.CompletionStage; + +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.stub.ServerCalls; +import io.grpc.stub.StreamObserver; + +/** + * A gRPC method call handler. + * + * @param the request type + * @param the response type + */ +public interface MethodHandler + extends ServerCalls.UnaryMethod, + ServerCalls.ClientStreamingMethod, + ServerCalls.ServerStreamingMethod, + ServerCalls.BidiStreamingMethod { + /** + * Obtain the {@link MethodDescriptor.MethodType gRPC method tyoe} that + * this {@link MethodHandler} handles. + * + * @return the {@link MethodDescriptor.MethodType gRPC method type} that + * this {@link MethodHandler} handles + */ + MethodDescriptor.MethodType type(); + + /** + * Obtain the request type. + * @return the request type + */ + Class getRequestType(); + + /** + * Obtain the response type. + * @return the response type + */ + Class getResponseType(); + + /** + * Obtain the name of the underlying Java method that this handler maps to. + * + * @return the name of the underlying Java method that this handler maps to + */ + String javaMethodName(); + + /** + * Determine whether this is a client side only handler. + * + * @return {@code true} if this handler can only be used on the client + */ + default boolean clientOnly() { + return false; + } + + @Override + default void invoke(ReqT request, StreamObserver observer) { + observer.onError(Status.UNIMPLEMENTED.asException()); + } + + @Override + default StreamObserver invoke(StreamObserver observer) { + observer.onError(Status.UNIMPLEMENTED.asException()); + return null; + } + + /** + * Handle a bi-directional client call. + * + * @param args the call arguments. + * @param client the {@link BidirectionalClient} instance to forward the call to. + * @return the call result + */ + default Object bidirectional(Object[] args, BidirectionalClient client) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + /** + * Handle a bi-directional client call. + * + * @param args the call arguments. + * @param client the {@link ClientStreaming} instance to forward the call to. + * @return the call result + */ + default Object clientStreaming(Object[] args, ClientStreaming client) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + /** + * Handle a bi-directional client call. + * + * @param args the call arguments. + * @param client the {@link ServerStreamingClient} instance to forward the call to. + * @return the call result + */ + default Object serverStreaming(Object[] args, ServerStreamingClient client) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + /** + * Handle a bi-directional client call. + * + * @param args the call arguments. + * @param client the {@link UnaryClient} instance to forward the call to. + * @return the call result + */ + default Object unary(Object[] args, UnaryClient client) { + throw Status.UNIMPLEMENTED.asRuntimeException(); + } + + /** + * A bidirectional client call handler. + */ + interface BidirectionalClient { + /** + * Perform a bidirectional client call. + * + * @param methodName the name of the gRPC method + * @param observer the {@link StreamObserver} that will receive the responses + * @param the request type + * @param the response type + * @return a {@link StreamObserver} to use to send requests + */ + StreamObserver bidiStreaming(String methodName, StreamObserver observer); + } + + /** + * A client streaming client call handler. + */ + interface ClientStreaming { + /** + * Perform a client streaming client call. + * + * @param methodName the name of the gRPC method + * @param observer the {@link StreamObserver} that will receive the responses + * @param the request type + * @param the response type + * @return a {@link StreamObserver} to use to send requests + */ + StreamObserver clientStreaming(String methodName, StreamObserver observer); + } + + /** + * A server streaming client call handler. + */ + interface ServerStreamingClient { + /** + * Perform a server streaming client call. + * + * @param methodName the name of the gRPC method + * @param request the request message + * @param observer the {@link StreamObserver} that will receive the responses + * @param the request type + * @param the response type + */ + void serverStreaming(String methodName, ReqT request, StreamObserver observer); + } + + /** + * A unary client call handler. + */ + interface UnaryClient { + /** + * Perform a unary client call. + * + * @param methodName the name of the gRPC method + * @param request the request message + * @param the request type + * @param the response type + * @return a {@link java.util.concurrent.CompletableFuture} that completes when the call completes + */ + CompletionStage unary(String methodName, ReqT request); + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java new file mode 100644 index 00000000000..7114abb16d2 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019, 2021 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.grpc.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Stream; + +// import io.helidon.common.Prioritized; +import jakarta.annotation.Priority; + +/** + * A bag of values ordered by priority. + *

+ * An element with lower priority number is more significant than an element + * with a higher priority number. + *

+ * For cases where priority is the same, elements are ordered in the order that + * they were added to the bag. + *

+ * Elements added with negative priorities are assumed to have no priority and + * will be least significant in order. + * + * @param the type of elements in the bag + */ +public class PriorityBag implements Iterable { + + private final Map> contents; + + private final List noPriorityList; + + private final int defaultPriority; + + private PriorityBag(Map> contents, List noPriorityList, int defaultPriority) { + this.contents = contents; + this.noPriorityList = noPriorityList; + this.defaultPriority = defaultPriority; + } + + /** + * Create a new {@link PriorityBag} where elements + * added with no priority will be last in the order. + * + * @param the type of elements in the bag + * @return a new {@link PriorityBag} where elements + * dded with no priority will be last in the + * order + */ + public static PriorityBag create() { + return new PriorityBag<>(new TreeMap<>(), new ArrayList<>(), -1); + } + + /** + * Create a new {@link PriorityBag} where elements + * added with no priority will be be given a default + * priority value. + * + * @param priority the default priority value to assign + * to elements added with no priority + * @param the type of elements in the bag + * + * @return a new {@link PriorityBag} where elements + * added with no priority will be be given + * a default priority value + */ + public static PriorityBag withDefaultPriority(int priority) { + return new PriorityBag<>(new TreeMap<>(), new ArrayList<>(), priority); + } + + + /** + * Obtain a copy of this {@link PriorityBag}. + * + * @return a copy of this {@link PriorityBag} + */ + public PriorityBag copyMe() { + PriorityBag copy = PriorityBag.create(); + copy.merge(this); + return copy; + } + + /** + * Obtain an immutable copy of this {@link PriorityBag}. + * + * @return an immutable copy of this {@link PriorityBag} + */ + public PriorityBag readOnly() { + return new PriorityBag<>(Collections.unmodifiableMap(contents), + Collections.unmodifiableList(noPriorityList), + defaultPriority); + } + + /** + * Merge a {@link PriorityBag} into this {@link PriorityBag}. + * + * @param bag the bag to merge + */ + public void merge(PriorityBag bag) { + bag.contents.forEach((priority, value) -> addAll(value, priority)); + this.noPriorityList.addAll(bag.noPriorityList); + } + + /** + * Add elements to the bag. + *

+ * If the element's class is annotated with the {@link jakarta.annotation.Priority} + * annotation then that value will be used to determine priority otherwise the + * default priority value will be used. + * + * @param values the elements to add + */ + public void addAll(Iterable values) { + for (T value : values) { + add(value); + } + } + + /** + * Add elements to the bag. + * + * @param values the elements to add + * @param priority the priority to assign to the elements + */ + public void addAll(Iterable values, int priority) { + for (T value : values) { + add(value, priority); + } + } + + /** + * Add an element to the bag. + *

+ * If the element's class is annotated with the {@link jakarta.annotation.Priority} + * annotation then that value will be used to determine priority otherwise the + * default priority value will be used. + * + * @param value the element to add + */ + public void add(T value) { + if (value != null) { + int priority; + // if (value instanceof Prioritized) { + // priority = ((Prioritized) value).priority(); + // } else { + Priority annotation = value.getClass().getAnnotation(Priority.class); + priority = annotation == null ? defaultPriority : annotation.value(); + // } + add(value, priority); + } + } + + /** + * Add an element to the bag with a specific priority. + *

+ * + * @param value the element to add + * @param priority the priority of the element + */ + public void add(T value, int priority) { + if (value != null) { + if (priority < 0) { + noPriorityList.add(value); + } else { + contents.compute(priority, (key, list) -> combine(list, value)); + } + } + } + + /** + * Obtain the contents of this {@link PriorityBag} as + * an ordered {@link Stream}. + * + * @return the contents of this {@link PriorityBag} as + * an ordered {@link Stream} + */ + public Stream stream() { + Stream stream = contents.entrySet() + .stream() + .flatMap(e -> e.getValue().stream()); + + return Stream.concat(stream, noPriorityList.stream()); + } + + @Override + public Iterator iterator() { + return stream().iterator(); + } + + private List combine(List list, T value) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(value); + return list; + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java new file mode 100644 index 00000000000..3878c748664 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java @@ -0,0 +1,455 @@ +/* + * Copyright (c) 2019, 2021 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.grpc.core; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import io.grpc.stub.StreamObserver; + +/** + * A number of helper methods to handle sending responses to a {@link StreamObserver}. + */ +public final class ResponseHelper { + private ResponseHelper() { + } + + /** + * Complete a gRPC request. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * specified value then calling {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to complete + * @param value the value to use when calling {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, T value) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + safe.onNext(value); + safe.onCompleted(); + } + + /** + * Complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, CompletionStage future) { + future.whenComplete(completeWithResult(observer)); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The execution will take place asynchronously on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, CompletionStage future) { + future.whenCompleteAsync(completeWithResult(observer)); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link CompletionStage} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param future the {@link CompletionStage} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, CompletionStage future, Executor executor) { + future.whenCompleteAsync(completeWithResult(observer), executor); + } + + /** + * Complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, Callable callable) { + try { + observer.onNext(callable.call()); + observer.onCompleted(); + } catch (Throwable t) { + observer.onError(t); + } + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The execution will take place asynchronously on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Callable callable) { + completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable))); + } + + /** + * Asynchronously complete a gRPC request based on the result of a {@link Callable}. + *

+ * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the + * result obtained on completion of the specified {@link Callable} and then calling + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param callable the {@link Callable} to use to obtain the value to use to call + * {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Callable callable, Executor executor) { + completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable), executor)); + } + + /** + * Execute a {@link Runnable} task and on completion of the task complete the gRPC request by + * calling {@link StreamObserver#onNext(Object)} using the specified result and then call + * {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void complete(StreamObserver observer, Runnable task, T result) { + complete(observer, Executors.callable(task, result)); + } + + /** + * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC + * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then + * call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + *

+ * The task and and request completion will be executed on the fork-join thread pool. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Runnable task, T result) { + completeAsync(observer, Executors.callable(task, result)); + } + + /** + * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC + * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then + * call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} + * will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param task the {@link Runnable} to execute + * @param result the result to pass to {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void completeAsync(StreamObserver observer, Runnable task, T result, Executor executor) { + completeAsync(observer, Executors.callable(task, result), executor); + } + + /** + * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the + * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void stream(StreamObserver observer, Stream stream) { + stream(observer, () -> stream); + } + + /** + * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until + * the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void streamAsync(StreamObserver observer, Stream stream, Executor executor) { + executor.execute(() -> stream(observer, () -> stream)); + } + + /** + * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the + * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param they type of the request result + */ + public static void stream(StreamObserver observer, Supplier> supplier) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + Throwable thrown = null; + + try { + supplier.get().forEach(safe::onNext); + } catch (Throwable t) { + thrown = t; + } + + if (thrown == null) { + safe.onCompleted(); + } else { + safe.onError(thrown); + } + } + + /** + * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method + * until the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. + *

+ * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. + * + * @param observer the {@link StreamObserver} to complete + * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + */ + public static void streamAsync(StreamObserver observer, Supplier> supplier, Executor executor) { + executor.execute(() -> stream(observer, supplier)); + } + + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then call {@link StreamObserver#onCompleted()}. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + // todo: a bit of a chicken or egg when used with Coherence streaming methods, isn't it? + public static Consumer stream(StreamObserver observer, CompletionStage stage) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenComplete(completeWithoutResult(safe)); + return safe::onNext; + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the + * fork-join thread pool. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + public static Consumer streamAsync(StreamObserver observer, CompletionStage stage) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenCompleteAsync(completeWithoutResult(safe)); + return value -> CompletableFuture.runAsync(() -> safe.onNext(value)); + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until + * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the executor + * thread. + *

+ * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} + * will be called instead of {@link StreamObserver#onCompleted()}. + * + * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes + * @param stage the {@link CompletionStage} to await completion of + * @param executor the {@link Executor} on which to execute the asynchronous + * request completion + * @param they type of the request result + * + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + public static Consumer streamAsync(StreamObserver observer, CompletionStage stage, Executor executor) { + StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); + stage.whenCompleteAsync(completeWithoutResult(safe), executor); + return value -> CompletableFuture.runAsync(() -> safe.onNext(value), executor); + } + + /** + * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method. + * @param observer the {@link StreamObserver} to complete + * @param the type of the result + * @param the type of the response + * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method + */ + public static BiConsumer completeWithResult(StreamObserver observer) { + return new CompletionAction<>(observer, true); + } + + /** + * Obtain a {@link Consumer} that can be used to complete a {@link StreamObserver}. + * @param observer the {@link StreamObserver} to complete + * @param the type of the response + * @return a {@link Consumer} that can be used to complete a {@link StreamObserver} + */ + public static BiConsumer completeWithoutResult(StreamObserver observer) { + return new CompletionAction<>(observer, false); + } + + /** + * Convert a {@link Callable} to a {@link Supplier}. + * @param callable the {@link Callable} to convert + * @param the result returned by the {@link Callable} + * @return a {@link Supplier} that wraps the {@link Callable} + */ + public static Supplier createSupplier(Callable callable) { + return new CallableSupplier<>(callable); + } + + /** + * A {@link BiConsumer} that is used to handle completion of a + * {@link CompletionStage} by forwarding + * the result to a {@link io.grpc.stub.StreamObserver}. + * + * @param the type of the {@link CompletionStage}'s result + * @param the type of result expected by the {@link io.grpc.stub.StreamObserver} + */ + private static class CompletionAction implements BiConsumer { + private StreamObserver observer; + private boolean sendResult; + + CompletionAction(StreamObserver observer, boolean sendResult) { + this.observer = observer; + this.sendResult = sendResult; + } + + @Override + @SuppressWarnings("unchecked") + public void accept(T result, Throwable error) { + if (error != null) { + observer.onError(error); + } else { + if (sendResult) { + observer.onNext((U) result); + } + observer.onCompleted(); + } + } + } + + /** + * A class that converts a {@link Callable} to a {@link Supplier}. + * @param the type of result returned from the callable + */ + private static class CallableSupplier implements Supplier { + private Callable callable; + + CallableSupplier(Callable callable) { + this.callable = callable; + } + + @Override + public T get() { + try { + return callable.call(); + } catch (Exception e) { + throw new CompletionException(e.getMessage(), e); + } + } + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java new file mode 100644 index 00000000000..9661a45be3f --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2019, 2021 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.grpc.core; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** + * A {@link io.grpc.stub.StreamObserver} that handles exceptions correctly. + * + * @param the type of response expected + */ +public class SafeStreamObserver + implements StreamObserver { + + /** + * Create a {@link SafeStreamObserver} that wraps + * another {@link io.grpc.stub.StreamObserver}. + * + * @param streamObserver the {@link io.grpc.stub.StreamObserver} to wrap + */ + private SafeStreamObserver(StreamObserver streamObserver) { + delegate = streamObserver; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (t == null) { + onError(Status.INVALID_ARGUMENT + .withDescription("onNext called with null. Null values are generally not allowed.") + .asRuntimeException()); + } else { + try { + delegate.onNext(t); + } catch (Throwable thrown) { + throwIfFatal(thrown); + onError(thrown); + } + } + } + + @Override + public void onError(Throwable thrown) { + try { + if (done) { + LOGGER.log(Level.SEVERE, checkNotNull(thrown), () -> "OnError called after StreamObserver was closed"); + } else { + done = true; + delegate.onError(checkNotNull(thrown)); + } + } catch (Throwable t) { + throwIfFatal(t); + LOGGER.log(Level.SEVERE, t, () -> "Caught exception handling onError"); + } + } + + @Override + public void onCompleted() { + if (done) { + LOGGER.log(Level.WARNING, "onComplete called after StreamObserver was closed"); + } else { + try { + done = true; + delegate.onCompleted(); + } catch (Throwable thrown) { + throwIfFatal(thrown); + LOGGER.log(Level.SEVERE, thrown, () -> "Caught exception handling onComplete"); + } + } + } + + /** + * Obtain the wrapped {@link StreamObserver}. + * @return the wrapped {@link StreamObserver} + */ + public StreamObserver delegate() { + return delegate; + } + + private Throwable checkNotNull(Throwable thrown) { + if (thrown == null) { + thrown = Status.INVALID_ARGUMENT + .withDescription("onError called with null Throwable. Null exceptions are generally not allowed.") + .asRuntimeException(); + } + + return thrown; + } + + /** + * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These varieties are + * as follows: + *

    + *
  • {@code VirtualMachineError}
  • + *
  • {@code ThreadDeath}
  • + *
  • {@code LinkageError}
  • + *
+ * + * @param thrown the {@code Throwable} to test and perhaps throw + */ + private static void throwIfFatal(Throwable thrown) { + if (thrown instanceof VirtualMachineError) { + throw (VirtualMachineError) thrown; + } else if (thrown instanceof ThreadDeath) { + throw (ThreadDeath) thrown; + } else if (thrown instanceof LinkageError) { + throw (LinkageError) thrown; + } + } + + /** + * Ensure that the specified {@link StreamObserver} is a safe observer. + *

+ * If the specified observer is not an instance of {@link SafeStreamObserver} then wrap + * it in a {@link SafeStreamObserver}. + * + * @param observer the {@link StreamObserver} to test + * @param the response type expected by the observer + * + * @return a safe {@link StreamObserver} + */ + public static StreamObserver ensureSafeObserver(StreamObserver observer) { + if (observer instanceof SafeStreamObserver) { + return observer; + } + + return new SafeStreamObserver<>(observer); + } + + // ----- constants ------------------------------------------------------ + + /** + * The {2link Logger} to use. + */ + private static final Logger LOGGER = Logger.getLogger(SafeStreamObserver.class.getName()); + + // ----- data members --------------------------------------------------- + + /** + * The actual StreamObserver. + */ + private StreamObserver delegate; + + /** + * Indicates a terminal state. + */ + private boolean done; +} 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 new file mode 100644 index 00000000000..019189e5db5 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2021 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. + */ + +/** + * Core classes used by both the reactive gRPC server API and gRPC client API. + */ +package io.helidon.grpc.core; diff --git a/grpc/core/src/main/java/module-info.java b/grpc/core/src/main/java/module-info.java new file mode 100644 index 00000000000..a46489ed6ad --- /dev/null +++ b/grpc/core/src/main/java/module-info.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** + * Helidon GRPC core package. + */ +module io.helidon.grpc.core { + + requires transitive io.helidon.common.context; + requires transitive io.helidon.common.config; + requires transitive io.grpc; + requires transitive io.grpc.stub; + requires transitive com.google.protobuf; + requires transitive io.grpc.protobuf; + requires transitive io.grpc.protobuf.lite; + + requires java.logging; + requires io.helidon.tracing; + requires jakarta.inject; + requires jakarta.annotation; + + exports io.helidon.grpc.core; + +} diff --git a/grpc/pom.xml b/grpc/pom.xml new file mode 100644 index 00000000000..64b1c84c1a2 --- /dev/null +++ b/grpc/pom.xml @@ -0,0 +1,38 @@ + + + + + 4.0.0 + + io.helidon + helidon-project + 4.0.0-SNAPSHOT + + pom + + io.helidon.grpc + helidon-grpc-project + Helidon gRPC Project + + gRPC support for Helidon + + + core + + diff --git a/pom.xml b/pom.xml index db3d6bfeb05..bc27020a09a 100644 --- a/pom.xml +++ b/pom.xml @@ -203,6 +203,7 @@ dependencies fault-tolerance graphql + grpc health helidon http diff --git a/webclient/grpc/pom.xml b/webclient/grpc/pom.xml new file mode 100644 index 00000000000..8ddcaf6fccf --- /dev/null +++ b/webclient/grpc/pom.xml @@ -0,0 +1,146 @@ + + + + 4.0.0 + + io.helidon.webclient + helidon-webclient-project + 4.0.0-SNAPSHOT + + + helidon-webclient-grpc + Helidon WebClient gRPC + + + + io.grpc + grpc-core + + + io.grpc + grpc-stub + + + io.helidon.grpc + helidon-grpc-core + + + io.helidon.http + helidon-http-http2 + + + io.helidon.http.encoding + helidon-http-encoding + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webclient + helidon-webclient-http2 + + + io.helidon.common.features + helidon-common-features-api + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver + helidon-webserver-grpc + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-params + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + + + diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java new file mode 100644 index 00000000000..71982384ce2 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java @@ -0,0 +1,419 @@ +/* + * 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.webclient.grpc; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.helidon.grpc.core.InterceptorPriorities; +import io.helidon.grpc.core.MarshallerSupplier; +import io.helidon.grpc.core.MethodHandler; +import io.helidon.grpc.core.PriorityBag; + +/** + * Encapsulates all metadata necessary to define a gRPC method. In addition to wrapping a {@link io.grpc.MethodDescriptor}, + * this class also holds the request and response types of the gRPC method. A + * {@link io.helidon.webclient.grpc.GrpcServiceDescriptor} can contain zero or more {@link io.grpc.MethodDescriptor}. + *

+ * An instance of ClientMethodDescriptor can be created either from an existing {@link io.grpc.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 io.grpc.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 List interceptors; + + /** + * The {@link io.grpc.CallCredentials} for this method. + */ + private final CallCredentials callCredentials; + + /** + * The method handler for this method. + */ + private final MethodHandler methodHandler; + + private ClientMethodDescriptor(String name, + MethodDescriptor descriptor, + List interceptors, + CallCredentials callCredentials, + MethodHandler methodHandler) { + this.name = name; + this.descriptor = descriptor; + this.interceptors = interceptors; + this.callCredentials = callCredentials; + this.methodHandler = methodHandler; + } + + /** + * Creates a new {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with the specified name and {@link io.grpc.MethodDescriptor}. + * + * @param serviceName the name of the owning gRPC service + * @param name the simple method name + * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} + * @return A new instance of a {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + */ + static Builder builder(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { + return new Builder(serviceName, name, descriptor); + } + + /** + * Creates a new {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with the specified + * name and {@link io.grpc.MethodDescriptor}. + * + * @param serviceName the name of the owning gRPC service + * @param name the simple method name + * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} + * @return a new instance of a {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + */ + static ClientMethodDescriptor create(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { + return builder(serviceName, name, descriptor).build(); + } + + /** + * Creates a new unary {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + */ + static Builder unary(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.UNARY); + } + + /** + * Creates a new client Streaming {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + */ + static Builder clientStreaming(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.CLIENT_STREAMING); + } + + /** + * Creates a new server streaming {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + */ + static Builder serverStreaming(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.SERVER_STREAMING); + } + + /** + * Creates a new bidirectional {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + */ + static Builder bidirectional(String serviceName, String name) { + return builder(serviceName, name, MethodDescriptor.MethodType.BIDI_STREAMING); + } + + /** + * Return the {@link io.grpc.CallCredentials} set on this service. + * + * @return the {@link io.grpc.CallCredentials} set on this service + */ + public CallCredentials callCredentials() { + return this.callCredentials; + } + + /** + * Creates a new {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.grpc.MethodDescriptor} of this method. + * + * @param the request type + * @param the response type + * @return The {@link io.grpc.MethodDescriptor} of this method. + */ + @SuppressWarnings("unchecked") + public MethodDescriptor descriptor() { + return descriptor; + } + + public MethodDescriptor.MethodType type() { + return descriptor.getType(); + } + + /** + * Obtain the {@link io.grpc.ClientInterceptor}s to use for this method. + * + * @return the {@link io.grpc.ClientInterceptor}s to use for this method + */ + List interceptors() { + return Collections.unmodifiableList(interceptors); + } + + /** + * 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 io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + */ + Rules responseType(Class type); + + /** + * Register one or more {@link io.grpc.ClientInterceptor interceptors} for the method. + * + * @param interceptors the interceptor(s) to register + * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + */ + Rules intercept(ClientInterceptor... interceptors); + + /** + * Register one or more {@link io.grpc.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 io.grpc.ClientInterceptor}s to register + * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + */ + Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); + + /** + * Register the specified {@link io.grpc.CallCredentials} to be used for this method. This overrides + * any {@link io.grpc.CallCredentials} set on the {@link io.helidon.webclient.grpc.ClientMethodDescriptor}. + * + * @param callCredentials the {@link io.grpc.CallCredentials} to set. + * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.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 {2link MethodHandler} to use + * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + */ + Rules methodHandler(MethodHandler methodHandler); + } + + /** + * {@link io.grpc.MethodDescriptor} builder implementation. + */ + public static class Builder + implements Rules, io.helidon.common.Builder { + + private String name; + private MethodDescriptor.Builder descriptor; + private Class requestType; + private Class responseType; + private PriorityBag interceptors = PriorityBag.withDefaultPriority(InterceptorPriorities.USER); + private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); + 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.defaultInstance(); + } 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 io.helidon.webclient.grpc.ClientMethodDescriptor}. + * + * @return a new instance of {@link io.helidon.webclient.grpc.ClientMethodDescriptor} + */ + @Override + @SuppressWarnings("unchecked") + public ClientMethodDescriptor build() { + MarshallerSupplier supplier = this.marshallerSupplier; + + if (supplier == null) { + supplier = defaultMarshallerSupplier; + } + + if (requestType != null) { + descriptor.setRequestMarshaller(supplier.get(requestType)); + } + + if (responseType != null) { + descriptor.setResponseMarshaller(supplier.get(responseType)); + } + + return new ClientMethodDescriptor(name, + descriptor.build(), + interceptors.stream().toList(), + callCredentials, + methodHandler); + } + + } +} 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 new file mode 100644 index 00000000000..9eb07db371c --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClient.java @@ -0,0 +1,83 @@ +/* + * 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.webclient.grpc; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.spi.Protocol; + +/** + * gRPC client. + */ +@RuntimeType.PrototypedBy(GrpcClientConfig.class) +public interface GrpcClient extends RuntimeType.Api { + /** + * Protocol to use to obtain an instance of gRPC specific client from + * {@link io.helidon.webclient.api.WebClient#client(io.helidon.webclient.spi.Protocol)}. + */ + Protocol PROTOCOL = GrpcProtocolProvider::new; + + /** + * A new fluent API builder to customize client setup. + * + * @return a new builder + */ + static GrpcClientConfig.Builder builder() { + return GrpcClientConfig.builder(); + } + + /** + * Create a new instance with custom configuration. + * + * @param clientConfig HTTP/2 client configuration + * @return a new HTTP/2 client + */ + static GrpcClient create(GrpcClientConfig clientConfig) { + return new GrpcClientImpl(WebClient.create(it -> it.from(clientConfig)), clientConfig); + } + + /** + * Create a new instance customizing its configuration. + * + * @param consumer HTTP/2 client configuration + * @return a new HTTP/2 client + */ + static GrpcClient create(Consumer consumer) { + return create(GrpcClientConfig.builder() + .update(consumer) + .buildPrototype()); + } + + /** + * Create a new instance with default configuration. + * + * @return a new HTTP/2 client + */ + static GrpcClient create() { + return create(GrpcClientConfig.create()); + } + + /** + * Create a client for a specific service. The client will be backed by the same HTTP/2 client. + * + * @param descriptor descriptor to use + * @return client for the provided descriptor + */ + GrpcServiceClient serviceClient(GrpcServiceDescriptor descriptor); +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java new file mode 100644 index 00000000000..3d4506dcb02 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -0,0 +1,77 @@ +/* + * 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.webclient.grpc; + +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.webclient.http2.Http2Client; + +import io.grpc.ClientCall; +import io.grpc.Metadata; + +class GrpcClientCall extends ClientCall { + private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); + + private final AtomicReference> responseListener = new AtomicReference<>(); + private final Http2Client http2Client; + private final ClientMethodDescriptor method; + + GrpcClientCall(Http2Client http2Client, ClientMethodDescriptor method) { + this.http2Client = http2Client; + this.method = method; + } + + @Override + public void start(Listener responseListener, Metadata headers) { + // connect + // send headers + if (this.responseListener.compareAndSet(null, responseListener)) { + /* + Http2ClientConnection http2ClientRequest = http2Client + .post("") // must be post + .header(GRPC_CONTENT_TYPE) + .connect(); + */ + + } else { + throw new IllegalStateException("Response listener was already set"); + } + } + + @Override + public void request(int numMessages) { + + } + + @Override + public void cancel(String message, Throwable cause) { + + } + + @Override + public void halfClose() { + + } + + @Override + public void sendMessage(ReqT message) { + + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java new file mode 100644 index 00000000000..8d1821acfea --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java @@ -0,0 +1,37 @@ +/* + * 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.webclient.grpc; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.webclient.api.HttpClientConfig; + +/** + * Configuration of a grpc client. + */ +@Prototype.Blueprint +@Prototype.Configured +interface GrpcClientConfigBlueprint extends HttpClientConfig, Prototype.Factory { + /** + * WebSocket specific configuration. + * + * @return protocol specific configuration + */ + @Option.Default("create()") + @Option.Configured + GrpcClientProtocolConfig protocolConfig(); +} 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 new file mode 100644 index 00000000000..6eadc8044c3 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientImpl.java @@ -0,0 +1,42 @@ +/* + * 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.webclient.grpc; + +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.http2.Http2Client; + +class GrpcClientImpl implements GrpcClient { + private final WebClient webClient; + private final Http2Client http2Client; + private final GrpcClientConfig clientConfig; + + GrpcClientImpl(WebClient webClient, GrpcClientConfig clientConfig) { + this.webClient = webClient; + this.http2Client = webClient.client(Http2Client.PROTOCOL); + this.clientConfig = clientConfig; + } + + @Override + public GrpcClientConfig prototype() { + return clientConfig; + } + + @Override + public GrpcServiceClient serviceClient(GrpcServiceDescriptor descriptor) { + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java new file mode 100644 index 00000000000..f3248d2ba88 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java @@ -0,0 +1,34 @@ +/* + * 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.webclient.grpc; + +import io.helidon.common.socket.SocketContext; +import io.helidon.http.http2.Http2Settings; +import io.helidon.http.http2.Http2StreamState; +import io.helidon.http.http2.StreamFlowControl; +import io.helidon.webclient.http2.Http2ClientConfig; + +class GrpcClientProtocol { + static GrpcClientStream create(SocketContext scoketContext, + Http2Settings serverSettings, + Http2ClientConfig clientConfig, + int streamId, + StreamFlowControl flowControl, + Http2StreamState streamState) { + return new GrpcClientStream(); + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java new file mode 100644 index 00000000000..938277994e3 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.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.webclient.grpc; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.webclient.spi.ProtocolConfig; + +/** + * Configuration of an HTTP/1.1 client. + */ +@Prototype.Blueprint +@Prototype.Configured +interface GrpcClientProtocolConfigBlueprint extends ProtocolConfig { + @Override + default String type() { + return GrpcProtocolProvider.CONFIG_KEY; + } + + @Option.Configured + @Option.Default(GrpcProtocolProvider.CONFIG_KEY) + @Override + String name(); + +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java new file mode 100644 index 00000000000..1bac6c80f36 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java @@ -0,0 +1,75 @@ +/* + * 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.webclient.grpc; + +import io.helidon.common.buffers.BufferData; +import io.helidon.http.http2.Http2FrameHeader; +import io.helidon.http.http2.Http2Headers; +import io.helidon.http.http2.Http2Priority; +import io.helidon.http.http2.Http2RstStream; +import io.helidon.http.http2.Http2Stream; +import io.helidon.http.http2.Http2StreamState; +import io.helidon.http.http2.Http2WindowUpdate; +import io.helidon.http.http2.StreamFlowControl; +import io.helidon.webclient.api.ReleasableResource; + +class GrpcClientStream implements Http2Stream, ReleasableResource { + @Override + public boolean rstStream(Http2RstStream rstStream) { + return false; + } + + @Override + public void windowUpdate(Http2WindowUpdate windowUpdate) { + + } + + @Override + public void headers(Http2Headers headers, boolean endOfStream) { + + } + + @Override + public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) { + + } + + @Override + public void priority(Http2Priority http2Priority) { + + } + + @Override + public int streamId() { + return 0; + } + + @Override + public Http2StreamState streamState() { + return null; + } + + @Override + public StreamFlowControl flowControl() { + return null; + } + + @Override + public void closeResource() { + + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java new file mode 100644 index 00000000000..a6b43a5ba63 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java @@ -0,0 +1,45 @@ +/* + * 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.webclient.grpc; + +import io.helidon.common.config.Config; +import io.helidon.webclient.spi.ProtocolConfigProvider; + +/** + * Implementation of protocol config provider for gRPC. + */ +public class GrpcProtocolConfigProvider implements ProtocolConfigProvider { + /** + * Required to be used by {@link java.util.ServiceLoader}. + * @deprecated do not use directly, use Http1ClientProtocol + */ + public GrpcProtocolConfigProvider() { + } + + @Override + public String configKey() { + return GrpcProtocolProvider.CONFIG_KEY; + } + + @Override + public GrpcClientProtocolConfig create(Config config, String name) { + return GrpcClientProtocolConfig.builder() + .config(config) + .name(name) + .build(); + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java new file mode 100644 index 00000000000..b0555ac0c49 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java @@ -0,0 +1,53 @@ +/* + * 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.webclient.grpc; + +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.spi.ClientProtocolProvider; + +public class GrpcProtocolProvider implements ClientProtocolProvider { + static final String CONFIG_KEY = "grpc"; + + /** + * Public constructor required by {@link java.util.ServiceLoader}. + */ + public GrpcProtocolProvider() { + } + + @Override + public String protocolId() { + return "grpc"; + } + + @Override + public Class configType() { + return GrpcClientProtocolConfig.class; + } + + @Override + public GrpcClientProtocolConfig defaultConfig() { + return GrpcClientProtocolConfig.create(); + } + + @Override + public GrpcClient protocol(WebClient client, GrpcClientProtocolConfig config) { + return new GrpcClientImpl(client, + GrpcClientConfig.builder().from(client.prototype()) + .protocolConfig(config) + .buildPrototype()); + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java new file mode 100644 index 00000000000..0a578892e78 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java @@ -0,0 +1,51 @@ +/* + * 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.webclient.grpc; + +import java.util.Collection; + +import io.grpc.stub.StreamObserver; + +/** + * Client for a single service. + * + * @see io.helidon.webclient.grpc.GrpcClient#serviceClient(io.helidon.webclient.grpc.GrpcServiceDescriptor) + */ +public interface GrpcServiceClient { + /** + * Name of the service this client was created for. + * + * @return service name + */ + String serviceName(); + + RespT unary(String methodName, ReqT request); + + StreamObserver unary(String methodName, StreamObserver responseObserver); + + Collection serverStream(String methodName, ReqT request); + + void serverStream(String methodName, ReqT request, StreamObserver responseObserver); + + RespT clientStream(String methodName, Collection request); + + StreamObserver clientStream(String methodName, StreamObserver responseObserver); + + Collection bidi(String methodName, Collection responseObserver); + + StreamObserver bidi(String methodName, StreamObserver responseObserver); +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java new file mode 100644 index 00000000000..7a9ba194c27 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -0,0 +1,97 @@ +/* + * 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.webclient.grpc; + +import java.util.Collection; + +import io.grpc.stub.StreamObserver; +import io.helidon.webclient.http2.Http2Client; + +import io.grpc.ClientCall; +import io.grpc.MethodDescriptor; +import io.grpc.stub.ClientCalls; + +class GrpcServiceClientImpl implements GrpcServiceClient { + private final GrpcServiceDescriptor descriptor; + private final Http2Client http2Client; + + GrpcServiceClientImpl(GrpcServiceDescriptor descriptor, Http2Client http2Client) { + this.descriptor = descriptor; + this.http2Client = http2Client; + } + + @Override + public String serviceName() { + return null; + } + + @Override + public RespT unary(String methodName, ReqT request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.UNARY); + return ClientCalls.blockingUnaryCall(call, request); + } + + @Override + public StreamObserver unary(String methodName, StreamObserver responseObserver) { + return null; + } + + @Override + public Collection serverStream(String methodName, ReqT request) { + return null; + } + + @Override + public void serverStream(String methodName, ReqT request, StreamObserver responseObserver) { + + } + + @Override + public RespT clientStream(String methodName, Collection request) { + return null; + } + + @Override + public StreamObserver clientStream(String methodName, StreamObserver responseObserver) { + return null; + } + + @Override + public Collection bidi(String methodName, Collection responseObserver) { + return null; + } + + @Override + public StreamObserver bidi(String methodName, StreamObserver responseObserver) { + return null; + } + + private ClientCall ensureMethod(String methodName, MethodDescriptor.MethodType methodType) { + ClientMethodDescriptor method = descriptor.method(methodName); + + if (!method.type().equals(methodType)) { + throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + ", yet " + methodType + " was requested."); + } + + return createClientCall(method); + } + + private ClientCall createClientCall(ClientMethodDescriptor method) { + + return new GrpcClientCall<>(http2Client, method); + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java new file mode 100644 index 00000000000..f96b71a766a --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java @@ -0,0 +1,42 @@ +/* + * 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.webclient.grpc; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; + +/** + * All required meta-data about a client side gRPC service. + */ +public class GrpcServiceDescriptor { + private String serviceName; + private Map methods; + private List interceptors; + private CallCredentials callCredentials; + + ClientMethodDescriptor method(String name) { + ClientMethodDescriptor clientMethodDescriptor = methods.get(name); + if (clientMethodDescriptor == null) { + throw new NoSuchElementException("There is no method " + name + " defined for service " + this); + } + return clientMethodDescriptor; + } +} diff --git a/webclient/grpc/src/main/java/module-info.java b/webclient/grpc/src/main/java/module-info.java new file mode 100644 index 00000000000..a13e5880898 --- /dev/null +++ b/webclient/grpc/src/main/java/module-info.java @@ -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. + */ + +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.HelidonFlavor; + +/** + * Helidon WebClient gRPC Support. + */ +@Feature(value = "gRPC", + description = "WebClient gRPC support", + in = HelidonFlavor.SE, + path = {"WebClient", "gRPC"} +) +module io.helidon.webclient.grpc { + + requires static io.helidon.common.features.api; + + requires transitive io.grpc; + requires transitive io.grpc.stub; + requires transitive io.helidon.builder.api; + requires transitive io.helidon.common.pki; + requires transitive io.helidon.webclient.http2; + requires transitive io.helidon.webclient; + + requires io.helidon.grpc.core; + + exports io.helidon.webclient.grpc; + + provides io.helidon.webclient.spi.ClientProtocolProvider + with io.helidon.webclient.grpc.GrpcProtocolProvider; + provides io.helidon.webclient.spi.ProtocolConfigProvider + with io.helidon.webclient.grpc.GrpcProtocolConfigProvider; +} \ No newline at end of file diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java index c1d35f6c7a3..286731ba1d9 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java @@ -22,6 +22,7 @@ import io.helidon.common.config.Config; import io.helidon.webclient.api.HttpClient; import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.spi.Protocol; /** * HTTP2 client. @@ -32,6 +33,11 @@ public interface Http2Client extends HttpClient, RuntimeType * HTTP/2 protocol ID, as used by ALPN. */ String PROTOCOL_ID = "h2"; + /** + * Protocol to use to obtain an instance of http/2 specific client from + * {@link io.helidon.webclient.api.WebClient#client(io.helidon.webclient.spi.Protocol)}. + */ + Protocol PROTOCOL = Http2ProtocolProvider::new; /** * A new fluent API builder to customize client setup. diff --git a/webclient/pom.xml b/webclient/pom.xml index 818b8621299..e2ee8899811 100644 --- a/webclient/pom.xml +++ b/webclient/pom.xml @@ -41,6 +41,7 @@ tracing webclient websocket + grpc diff --git a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java index 1070e29ba50..007dd200af1 100644 --- a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java +++ b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java @@ -31,7 +31,7 @@ @RuntimeType.PrototypedBy(WsClientConfig.class) public interface WsClient extends RuntimeType.Api { /** - * Protocol to use to obtain an instance of WebSocket specific clietn from + * Protocol to use to obtain an instance of WebSocket specific client from * {@link io.helidon.webclient.api.WebClient#client(io.helidon.webclient.spi.Protocol)}. */ Protocol PROTOCOL = WsProtocolProvider::new; diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java index 0dcc498eaea..f9ff3d2eb4a 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java @@ -54,6 +54,15 @@ static Grpc unary(Descriptors.FileDescriptor proto, return grpc(proto, serviceName, methodName, ServerCalls.asyncUnaryCall(method)); } + static Grpc unary(Descriptors.FileDescriptor proto, + String serviceName, + String methodName, + GrpcServerCalls.Unary method) { + + return grpc(proto, serviceName, methodName, GrpcServerCalls.unaryCall(method)); + } + + static Grpc bidi(Descriptors.FileDescriptor proto, String serviceName, String methodName, @@ -136,17 +145,17 @@ private static Grpc grpc(Descriptors.FileDescriptor pro - to invoke a static method on it */ Class requestType = load(getClassName(mtd.getInputType())); - Class responsetype = load(getClassName(mtd.getOutputType())); + Class responseType = load(getClassName(mtd.getOutputType())); MethodDescriptor.Marshaller reqMarshaller = ProtoMarshaller.get(requestType); - MethodDescriptor.Marshaller resMarshaller = ProtoMarshaller.get(responsetype); + MethodDescriptor.Marshaller resMarshaller = ProtoMarshaller.get(responseType); io.grpc.MethodDescriptor.Builder grpcDesc = io.grpc.MethodDescriptor.newBuilder() .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName(serviceName, methodName)) .setType(getMethodType(mtd)).setFullMethodName(path).setRequestMarshaller(reqMarshaller) .setResponseMarshaller(resMarshaller).setSampledToLocalTracing(true); - return new Grpc<>(grpcDesc.build(), PathMatchers.exact(path), requestType, responsetype, callHandler); + return new Grpc<>(grpcDesc.build(), PathMatchers.exact(path), requestType, responseType, callHandler); } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java index 3c7d108a0ad..c625af21e65 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java @@ -65,7 +65,7 @@ public SubProtocolResult subProtocol(ConnectionContext ctx, Headers httpHeaders = headers.httpHeaders(); if (httpHeaders.contains(HeaderNames.CONTENT_TYPE)) { - String contentType = httpHeaders.get(HeaderNames.CONTENT_TYPE).value(); + String contentType = httpHeaders.get(HeaderNames.CONTENT_TYPE).get(); if (contentType.startsWith("application/grpc")) { GrpcRouting routing = router.routing(GrpcRouting.class, GrpcRouting.empty()); diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java index 24fa0ce724c..86ee4f5027a 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java @@ -133,6 +133,25 @@ public Builder unary(Descriptors.FileDescriptor proto, return route(Grpc.unary(proto, serviceName, methodName, method)); } + /** + * Unary route. + * + * @param proto proto descriptor + * @param serviceName service name + * @param methodName method name + * @param method method to handle this route + * @param request type + * @param response type + * @return updated builder + */ + public Builder unary(Descriptors.FileDescriptor proto, + String serviceName, + String methodName, + GrpcServerCalls.Unary method) { + + return route(Grpc.unary(proto, serviceName, methodName, method)); + } + /** * Bidirectional route. * diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java new file mode 100644 index 00000000000..c5e72a39b54 --- /dev/null +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java @@ -0,0 +1,137 @@ +package io.helidon.webserver.grpc; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import io.grpc.ServerCallHandler; +import io.grpc.stub.ServerCalls; +import io.grpc.stub.StreamObserver; + +public final class GrpcServerCalls { + private GrpcServerCalls() { + } + + static ServerCallHandler unaryCall(Unary method) { + return ServerCalls.asyncUnaryCall((request, responseObserver) -> { + try { + ResT response = method.invoke(request); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } catch (Exception e) { + responseObserver.onError(e); + } + }); + } + + static ServerCallHandler clientStream(ClientStream method, Duration timeout) { + return ServerCalls.asyncClientStreamingCall(responseObserver -> { + CompletableFuture> future = new CompletableFuture<>(); + + future.orTimeout(timeout.getNano(), TimeUnit.NANOSECONDS) + .thenAccept(requests -> { + responseObserver.onNext(method.invoke(requests)); + responseObserver.onCompleted(); + }) + .exceptionally(throwable -> { + responseObserver.onError(throwable); + return null; + }); + + return new CollectingObserver<>(future); + }); + } + + static ServerCallHandler serverStream(ServerStream method) { + return ServerCalls.asyncServerStreamingCall((request, responseObserver) -> { + try { + Collection response = method.invoke(request); + for (ResT resT : response) { + responseObserver.onNext(resT); + } + responseObserver.onCompleted(); + } catch (Exception e) { + responseObserver.onError(e); + } + }); + } + + static ServerCallHandler bidi(Bidi method, Duration timeout) { + return ServerCalls.asyncBidiStreamingCall(responseObserver -> { + CompletableFuture> future = new CompletableFuture<>(); + + future.orTimeout(timeout.getNano(), TimeUnit.NANOSECONDS) + .thenAccept(requests -> { + Collection response = method.invoke(requests); + response.forEach(responseObserver::onNext); + responseObserver.onCompleted(); + }) + .exceptionally(throwable -> { + responseObserver.onError(throwable); + return null; + }); + + return new CollectingObserver<>(future); + }); + } + + public interface Unary { + RespT invoke(ReqT request); + } + + public interface ServerStream { + Collection invoke(ReqT request); + } + + public interface ClientStream { + RespT invoke(Collection requests); + } + + /** + * Bidirectional streaming is by its design created for asynchronous communication. + * This interface should be used only when you have a guarantee that the client sends all of its messages + * and DOES NOT WAIT for the responses on each of them. + *

+ * In case you need true asynchronous communication (e.g. clients sends a message, waits for server response, + * send another one), + * please use {@link io.grpc.stub.ServerCalls#asyncBidiStreamingCall(io.grpc.stub.ServerCalls.BidiStreamingMethod)}. + * + * @param request type + * @param response type + */ + public interface Bidi { + Collection invoke(Collection requests); + } + + /** + * Collects all elements (and possible exception) and completes the completable future when finished collecting. + * + * @param + */ + private static class CollectingObserver implements StreamObserver { + private final List collectedValues = new ArrayList<>(); + private final CompletableFuture> future; + + private CollectingObserver(CompletableFuture> future) { + this.future = future; + } + + @Override + public void onNext(T value) { + collectedValues.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(collectedValues); + } + } +} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java index 59581ae076b..a41f661a3a9 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java @@ -60,6 +60,7 @@ interface Routing { * @return updated routing */ Routing unary(String methodName, ServerCalls.UnaryMethod method); + Routing unary(String methodName, GrpcServerCalls.Unary method); /** * Bidirectional route. @@ -71,6 +72,7 @@ interface Routing { * @return updated routing */ Routing bidi(String methodName, ServerCalls.BidiStreamingMethod method); + Routing bidi(String methodName, GrpcServerCalls.Bidi method); /** * Server streaming route. @@ -82,6 +84,7 @@ interface Routing { * @return updated routing */ Routing serverStream(String methodName, ServerCalls.ServerStreamingMethod method); + Routing serverStream(String methodName, GrpcServerCalls.ServerStream method); /** * Client streaming route. @@ -93,5 +96,6 @@ interface Routing { * @return updated routing */ Routing clientStream(String methodName, ServerCalls.ClientStreamingMethod method); + Routing clientStream(String methodName, GrpcServerCalls.ClientStream method); } } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index f90927009fd..5c553223872 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -78,12 +78,22 @@ public GrpcService.Routing unary(String methodName, ServerCalls.Una return this; } + @Override + public GrpcService.Routing unary(String methodName, GrpcServerCalls.Unary method) { + return null; + } + @Override public GrpcService.Routing bidi(String methodName, ServerCalls.BidiStreamingMethod method) { routes.add(Grpc.bidi(proto, serviceName, methodName, method)); return this; } + @Override + public GrpcService.Routing bidi(String methodName, GrpcServerCalls.Bidi method) { + return null; + } + @Override public GrpcService.Routing serverStream(String methodName, ServerCalls.ServerStreamingMethod method) { @@ -91,6 +101,11 @@ public GrpcService.Routing serverStream(String methodName, return this; } + @Override + public GrpcService.Routing serverStream(String methodName, GrpcServerCalls.ServerStream method) { + return null; + } + @Override public GrpcService.Routing clientStream(String methodName, ServerCalls.ClientStreamingMethod method) { @@ -98,6 +113,11 @@ public GrpcService.Routing clientStream(String methodName, return this; } + @Override + public GrpcService.Routing clientStream(String methodName, GrpcServerCalls.ClientStream method) { + return null; + } + public GrpcServiceRoute build() { return new GrpcServiceRoute(serviceName, List.copyOf(routes)); } diff --git a/webserver/testing/junit5/grpc/pom.xml b/webserver/testing/junit5/grpc/pom.xml new file mode 100644 index 00000000000..92a63e7f6c8 --- /dev/null +++ b/webserver/testing/junit5/grpc/pom.xml @@ -0,0 +1,57 @@ + + + + + 4.0.0 + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-project + 4.0.0-SNAPSHOT + + + helidon-webserver-testing-junit5-grpc + Helidon WebServer Testing JUnit5 gRPC + + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + + + io.helidon.webclient + helidon-webclient-grpc + + + io.helidon.webserver + helidon-webserver-grpc + + + org.junit.jupiter + junit-jupiter-api + provided + + + org.hamcrest + hamcrest-all + provided + + + diff --git a/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java b/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java new file mode 100644 index 00000000000..90a44068812 --- /dev/null +++ b/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java @@ -0,0 +1,59 @@ +/* + * 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.webserver.testing.junit5.grpc; + +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testing.junit5.Junit5Util; +import io.helidon.webserver.testing.junit5.spi.ServerJunitExtension; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; + +/** + * A {@link java.util.ServiceLoader} provider implementation that adds support for injection of gRPC related + * artifacts, such as {@link io.helidon.webclient.grpc.GrpcClient} in Helidon integration tests. + */ +public class GrpcServerExtension implements ServerJunitExtension { + /** + * Required constructor for {@link java.util.ServiceLoader}. + */ + public GrpcServerExtension() { + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return GrpcClient.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, + ExtensionContext extensionContext, + Class parameterType, + WebServer server) { + String socketName = Junit5Util.socketName(parameterContext.getParameter()); + + if (GrpcClient.class.equals(parameterType)) { + return GrpcClient.builder() + .baseUri("http://localhost:" + server.port(socketName)) + .build(); + } + throw new ParameterResolutionException("gRPC extension only supports GrpcClient parameter type"); + } +} diff --git a/webserver/testing/junit5/grpc/src/main/java/module-info.java b/webserver/testing/junit5/grpc/src/main/java/module-info.java new file mode 100644 index 00000000000..81f02a3db8a --- /dev/null +++ b/webserver/testing/junit5/grpc/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/** + * Helidon WebServer Testing JUnit 5 Support for gRPC. + */ +module io.helidon.webserver.testing.junit5.grpc { + + requires io.helidon.webclient.grpc; + requires io.helidon.webserver.grpc; + + requires transitive io.helidon.webserver.testing.junit5; + + exports io.helidon.webserver.testing.junit5.grpc; + + provides io.helidon.webserver.testing.junit5.spi.ServerJunitExtension + with io.helidon.webserver.testing.junit5.grpc.GrpcServerExtension; + +} \ No newline at end of file diff --git a/webserver/testing/junit5/pom.xml b/webserver/testing/junit5/pom.xml index 00e10496c9f..0994ebe57cb 100644 --- a/webserver/testing/junit5/pom.xml +++ b/webserver/testing/junit5/pom.xml @@ -37,6 +37,7 @@ junit5 websocket http2 + grpc From 0d9378eef7048edc2fc8d31e4d100d206ce5f7ef Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 26 Jan 2024 17:02:48 -0500 Subject: [PATCH 02/38] Additional work arounds tests and descriptors. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/GrpcTest.java | 71 ++++++++ .../webserver/protocols/ProtocolsTest.java | 45 ----- webclient/grpc/pom.xml | 8 + .../webclient/grpc/GrpcClientCall.java | 107 +++++++++--- .../webclient/grpc/GrpcClientImpl.java | 10 +- ...r.java => GrpcClientMethodDescriptor.java} | 155 +++++++++--------- .../webclient/grpc/GrpcClientProtocol.java | 2 +- .../webclient/grpc/GrpcClientStream.java | 72 ++------ .../webclient/grpc/GrpcProtocolProvider.java | 2 +- .../webclient/grpc/GrpcServiceClientImpl.java | 19 +-- ...va => GrpcServiceDescriptorBlueprint.java} | 34 ++-- .../http2/Http2ClientConnection.java | 12 +- .../webclient/http2/Http2ClientImpl.java | 2 +- .../webclient/http2/Http2ClientStream.java | 5 +- .../webclient/http2/Http2ConnectionCache.java | 6 +- .../webclient/http2/Http2StreamConfig.java | 2 +- .../http2/LockingStreamIdSequence.java | 8 +- 17 files changed, 320 insertions(+), 240 deletions(-) create mode 100644 examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java delete mode 100644 examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java rename webclient/grpc/src/main/java/io/helidon/webclient/grpc/{ClientMethodDescriptor.java => GrpcClientMethodDescriptor.java} (65%) rename webclient/grpc/src/main/java/io/helidon/webclient/grpc/{GrpcServiceDescriptor.java => GrpcServiceDescriptorBlueprint.java} (61%) diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java new file mode 100644 index 00000000000..56e05ca422a --- /dev/null +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java @@ -0,0 +1,71 @@ +/* + * 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.webserver.protocols; + +import java.util.Locale; + +import com.google.protobuf.StringValue; +import io.helidon.examples.grpc.strings.Strings; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; +import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import org.junit.jupiter.api.Test; + +@ServerTest +class GrpcTest { + + private final GrpcClient grpcClient; + + private GrpcTest(GrpcClient grpcClient) { + this.grpcClient = grpcClient; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder builder) { + builder.addRouting(GrpcRouting.builder() + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + GrpcTest::blockingGrpcUpper)); + } + + private static Strings.StringMessage blockingGrpcUpper(Strings.StringMessage reqT) { + return Strings.StringMessage.newBuilder() + .setText(reqT.getText().toUpperCase(Locale.ROOT)) + .build(); + } + + @Test + void testSimpleCall() { + GrpcServiceDescriptor serviceDescriptor = + GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Upper", + GrpcClientMethodDescriptor.unary("StringService", "Upper") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .build(); + + String r = grpcClient.serviceClient(serviceDescriptor) + .unary("Upper", StringValue.of("hello")); + } +} diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java deleted file mode 100644 index 59beccb914e..00000000000 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/ProtocolsTest.java +++ /dev/null @@ -1,45 +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.webserver.protocols; - -import io.helidon.webclient.api.WebClient; -import io.helidon.webclient.grpc.GrpcClient; -import io.helidon.webclient.grpc.GrpcServiceDescriptor; -import io.helidon.webclient.websocket.WsClient; -import io.helidon.webserver.testing.junit5.ServerTest; -import org.junit.jupiter.api.Test; - -@ServerTest -class ProtocolsTest { - - private final WebClient webClient; - private final WsClient wsClient; - private final GrpcClient grpcClient; - - private ProtocolsTest(WebClient webClient, WsClient wsClient, GrpcClient grpcClient) { - this.webClient = webClient; - this.wsClient = wsClient; - this.grpcClient = grpcClient; - } - - @Test - void test() { - grpcClient.serviceClient( - GrpcServiceDescriptor - ) - } -} diff --git a/webclient/grpc/pom.xml b/webclient/grpc/pom.xml index 8ddcaf6fccf..d4630a4f5f6 100644 --- a/webclient/grpc/pom.xml +++ b/webclient/grpc/pom.xml @@ -140,6 +140,14 @@ + + io.helidon.build-tools + helidon-services-plugin + ${version.plugin.helidon-build-tools} + + fail + + diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 3d4506dcb02..8e4cc327c4e 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -16,40 +16,81 @@ package io.helidon.webclient.grpc; +import java.time.Duration; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import io.grpc.ClientCall; +import io.grpc.Metadata; import io.helidon.http.Header; import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; -import io.helidon.webclient.http2.Http2Client; - -import io.grpc.ClientCall; -import io.grpc.Metadata; - +import io.helidon.http.http2.Http2Settings; +import io.helidon.webclient.api.ClientConnection; +import io.helidon.webclient.api.ClientUri; +import io.helidon.webclient.api.ConnectionKey; +import io.helidon.webclient.api.DefaultDnsResolver; +import io.helidon.webclient.api.DnsAddressLookup; +import io.helidon.webclient.api.TcpClientConnection; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.http2.Http2ClientConnection; +import io.helidon.webclient.http2.Http2ClientImpl; +import io.helidon.webclient.http2.Http2StreamConfig; + +/** + * A gRPC client call handler. The typical order of calls will be: + * + * start request* sendMessage* halfClose + * + * @param + * @param + */ class GrpcClientCall extends ClientCall { private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); private final AtomicReference> responseListener = new AtomicReference<>(); - private final Http2Client http2Client; - private final ClientMethodDescriptor method; + private final GrpcClientImpl grpcClient; + private final GrpcClientMethodDescriptor method; + private final AtomicInteger messages = new AtomicInteger(); - GrpcClientCall(Http2Client http2Client, ClientMethodDescriptor method) { - this.http2Client = http2Client; + GrpcClientCall(GrpcClientImpl grpcClient, GrpcClientMethodDescriptor method) { + this.grpcClient = grpcClient; this.method = method; } @Override public void start(Listener responseListener, Metadata headers) { - // connect - // send headers if (this.responseListener.compareAndSet(null, responseListener)) { - /* - Http2ClientConnection http2ClientRequest = http2Client - .post("") // must be post - .header(GRPC_CONTENT_TYPE) - .connect(); - */ - + // obtain HTTP2 connection + Http2ClientConnection connection = Http2ClientConnection.create( + (Http2ClientImpl) grpcClient.http2Client(), clientConnection(), true); + + // create HTTP2 stream from connection + GrpcClientStream clientStream = new GrpcClientStream( + connection, + Http2Settings.create(), // Http2Settings + null, // SocketContext + new Http2StreamConfig() { + @Override + public boolean priorKnowledge() { + return true; + } + + @Override + public int priority() { + return 0; + } + + @Override + public Duration readTimeout() { + return grpcClient.prototype().readTimeout().orElse(null); + } + }, + null, // Http2ClientConfig + connection.streamIdSequence()); + + // send HEADERS frame } else { throw new IllegalStateException("Response listener was already set"); } @@ -57,21 +98,45 @@ public void start(Listener responseListener, Metadata headers) { @Override public void request(int numMessages) { - + messages.addAndGet(numMessages); } @Override public void cancel(String message, Throwable cause) { - + // close the stream/connection via RST_STREAM + // can be closed even if halfClosed } @Override public void halfClose() { - + // close the stream/connection + // GOAWAY frame } @Override public void sendMessage(ReqT message) { + // send a DATA frame + } + private ClientConnection clientConnection() { + GrpcClientConfig clientConfig = grpcClient.prototype(); + ClientUri clientUri = clientConfig.baseUri().orElseThrow(); + WebClient webClient = grpcClient.webClient(); + + ConnectionKey connectionKey = new ConnectionKey( + clientUri.scheme(), + clientUri.host(), + clientUri.port(), + clientConfig.readTimeout().orElse(null), + null, + DefaultDnsResolver.create(), + DnsAddressLookup.defaultLookup(), + null); + + return TcpClientConnection.create(webClient, + connectionKey, + Collections.emptyList(), + connection -> false, + connection -> {}).connect(); } } 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 6eadc8044c3..bf0934074d6 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 @@ -30,6 +30,14 @@ class GrpcClientImpl implements GrpcClient { this.clientConfig = clientConfig; } + public WebClient webClient() { + return webClient; + } + + public Http2Client http2Client() { + return http2Client; + } + @Override public GrpcClientConfig prototype() { return clientConfig; @@ -37,6 +45,6 @@ public GrpcClientConfig prototype() { @Override public GrpcServiceClient serviceClient(GrpcServiceDescriptor descriptor) { - throw new UnsupportedOperationException("Not implemented"); + return new GrpcServiceClientImpl(descriptor, this); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java similarity index 65% rename from webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java rename to webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index 71982384ce2..81a1630dbda 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -29,15 +29,17 @@ import io.helidon.grpc.core.PriorityBag; /** - * Encapsulates all metadata necessary to define a gRPC method. In addition to wrapping a {@link io.grpc.MethodDescriptor}, - * this class also holds the request and response types of the gRPC method. A - * {@link io.helidon.webclient.grpc.GrpcServiceDescriptor} can contain zero or more {@link io.grpc.MethodDescriptor}. + * Encapsulates all metadata necessary to define a gRPC method. In addition to wrapping + * a {@link io.grpc.MethodDescriptor}, this class also holds the request and response + * types of the gRPC method. A {@link io.helidon.webclient.grpc.GrpcServiceDescriptor} + * can contain zero or more {@link io.grpc.MethodDescriptor}. *

- * An instance of ClientMethodDescriptor can be created either from an existing {@link io.grpc.MethodDescriptor} or - * from one of the factory methods {@link #bidirectional(String, String)}, {@link #clientStreaming(String, String)}, + * An instance of ClientMethodDescriptor can be created either from an existing + * {@link io.grpc.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 { +public final class GrpcClientMethodDescriptor { /** * The simple name of the method. @@ -45,10 +47,10 @@ public final class ClientMethodDescriptor { private final String name; /** - * The {@link io.grpc.MethodDescriptor} for this method. This is usually obtained from protocol buffer - * method getDescriptor (from service getDescriptor). + * The {@link io.grpc.MethodDescriptor} for this method. This is usually obtained from + * protocol buffer method getDescriptor (from service getDescriptor). */ - private final MethodDescriptor descriptor; + private final MethodDescriptor descriptor; /** * The list of client interceptors for this method. @@ -63,13 +65,13 @@ public final class ClientMethodDescriptor { /** * The method handler for this method. */ - private final MethodHandler methodHandler; + private final MethodHandler methodHandler; - private ClientMethodDescriptor(String name, - MethodDescriptor descriptor, - List interceptors, - CallCredentials callCredentials, - MethodHandler methodHandler) { + private GrpcClientMethodDescriptor(String name, + MethodDescriptor descriptor, + List interceptors, + CallCredentials callCredentials, + MethodHandler methodHandler) { this.name = name; this.descriptor = descriptor; this.interceptors = interceptors; @@ -78,79 +80,80 @@ private ClientMethodDescriptor(String name, } /** - * Creates a new {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with the specified name and {@link io.grpc.MethodDescriptor}. + * Creates a new {@link GrpcClientMethodDescriptor.Builder} with the + * specified name and {@link io.grpc.MethodDescriptor}. * * @param serviceName the name of the owning gRPC service * @param name the simple method name * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} - * @return A new instance of a {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return A new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static Builder builder(String serviceName, + public static Builder builder(String serviceName, String name, - MethodDescriptor.Builder descriptor) { + MethodDescriptor.Builder descriptor) { return new Builder(serviceName, name, descriptor); } /** - * Creates a new {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with the specified - * name and {@link io.grpc.MethodDescriptor}. + * Creates a new {@link GrpcClientMethodDescriptor.Builder} with the + * specified name and {@link io.grpc.MethodDescriptor}. * * @param serviceName the name of the owning gRPC service * @param name the simple method name * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} - * @return a new instance of a {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static ClientMethodDescriptor create(String serviceName, - String name, - MethodDescriptor.Builder descriptor) { + public static GrpcClientMethodDescriptor create(String serviceName, + String name, + MethodDescriptor.Builder descriptor) { return builder(serviceName, name, descriptor).build(); } /** - * Creates a new unary {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with + * Creates a new unary {@link GrpcClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static Builder unary(String serviceName, String name) { + public static Builder unary(String serviceName, String name) { return builder(serviceName, name, MethodDescriptor.MethodType.UNARY); } /** - * Creates a new client Streaming {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with + * Creates a new client Streaming {@link GrpcClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static Builder clientStreaming(String serviceName, String name) { + public static Builder clientStreaming(String serviceName, String name) { return builder(serviceName, name, MethodDescriptor.MethodType.CLIENT_STREAMING); } /** - * Creates a new server streaming {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with + * Creates a new server streaming {@link GrpcClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static Builder serverStreaming(String serviceName, String name) { + public static Builder serverStreaming(String serviceName, String name) { return builder(serviceName, name, MethodDescriptor.MethodType.SERVER_STREAMING); } /** - * Creates a new bidirectional {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with + * Creates a new bidirectional {@link GrpcClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static Builder bidirectional(String serviceName, String name) { + public static Builder bidirectional(String serviceName, String name) { return builder(serviceName, name, MethodDescriptor.MethodType.BIDI_STREAMING); } @@ -164,21 +167,17 @@ public CallCredentials callCredentials() { } /** - * Creates a new {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} with the specified name. + * Creates a new {@link GrpcClientMethodDescriptor.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 io.helidon.webclient.grpc.ClientMethodDescriptor.Builder} + * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ - static Builder builder(String serviceName, - String name, - MethodDescriptor.MethodType methodType) { - - MethodDescriptor.Builder builder = MethodDescriptor.newBuilder() + public 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); @@ -200,9 +199,8 @@ public String name() { * @param the response type * @return The {@link io.grpc.MethodDescriptor} of this method. */ - @SuppressWarnings("unchecked") public MethodDescriptor descriptor() { - return descriptor; + return (MethodDescriptor) descriptor; } public MethodDescriptor.MethodType type() { @@ -223,7 +221,7 @@ List interceptors() { * * @return the {@link MethodHandler} to use to make client calls */ - public MethodHandler methodHandler() { + public MethodHandler methodHandler() { return methodHandler; } @@ -236,23 +234,26 @@ public interface Rules { * Sets the type of parameter of this method. * * @param type The type of parameter of this method. - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} instance for + * fluent call chaining */ - Rules requestType(Class type); + Rules requestType(Class type); /** * Sets the type of parameter of this method. * * @param type The type of parameter of this method. - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} instance for + * fluent call chaining */ - Rules responseType(Class type); + Rules responseType(Class type); /** * Register one or more {@link io.grpc.ClientInterceptor interceptors} for the method. * * @param interceptors the interceptor(s) to register - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} instance for + * fluent call chaining */ Rules intercept(ClientInterceptor... interceptors); @@ -263,7 +264,8 @@ public interface Rules { * * @param priority the priority to assign to the interceptors * @param interceptors one or more {@link io.grpc.ClientInterceptor}s to register - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} to allow fluent method chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} to allow + * fluent method chaining */ Rules intercept(int priority, ClientInterceptor... interceptors); @@ -273,16 +275,19 @@ public interface Rules { * If not set the default {@link MarshallerSupplier} from the service will be used. * * @param marshallerSupplier the {@link MarshallerSupplier} for the service - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} instance + * for fluent call chaining */ Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); /** - * Register the specified {@link io.grpc.CallCredentials} to be used for this method. This overrides - * any {@link io.grpc.CallCredentials} set on the {@link io.helidon.webclient.grpc.ClientMethodDescriptor}. + * Register the specified {@link io.grpc.CallCredentials} to be used for this method. + * This overrides any {@link io.grpc.CallCredentials} set on the + * {@link GrpcClientMethodDescriptor}. * * @param callCredentials the {@link io.grpc.CallCredentials} to set. - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} instance + * for fluent call chaining */ Rules callCredentials(CallCredentials callCredentials); @@ -290,26 +295,27 @@ public interface Rules { * Set the {@link MethodHandler} that can be used to invoke the method. * * @param methodHandler the {2link MethodHandler} to use - * @return this {@link io.helidon.webclient.grpc.ClientMethodDescriptor.Rules} instance for fluent call chaining + * @return this {@link GrpcClientMethodDescriptor.Rules} instance + * for fluent call chaining */ - Rules methodHandler(MethodHandler methodHandler); + Rules methodHandler(MethodHandler methodHandler); } /** * {@link io.grpc.MethodDescriptor} builder implementation. */ public static class Builder - implements Rules, io.helidon.common.Builder { + implements Rules, io.helidon.common.Builder { private String name; - private MethodDescriptor.Builder descriptor; + private final MethodDescriptor.Builder descriptor; private Class requestType; private Class responseType; - private PriorityBag interceptors = PriorityBag.withDefaultPriority(InterceptorPriorities.USER); + private final PriorityBag interceptors = PriorityBag.withDefaultPriority(InterceptorPriorities.USER); private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); private MarshallerSupplier marshallerSupplier; private CallCredentials callCredentials; - private MethodHandler methodHandler; + private MethodHandler methodHandler; /** * Constructs a new Builder instance. @@ -318,19 +324,19 @@ public static class Builder * @param name the name of this method * @param descriptor The gRPC method descriptor builder */ - Builder(String serviceName, String name, MethodDescriptor.Builder descriptor) { + Builder(String serviceName, String name, MethodDescriptor.Builder descriptor) { this.name = name; this.descriptor = descriptor.setFullMethodName(serviceName + "/" + name); } @Override - public Builder requestType(Class type) { + public Builder requestType(Class type) { this.requestType = type; return this; } @Override - public Builder responseType(Class type) { + public Builder responseType(Class type) { this.responseType = type; return this; } @@ -363,7 +369,7 @@ Builder defaultMarshallerSupplier(MarshallerSupplier supplier) { } @Override - public Builder methodHandler(MethodHandler methodHandler) { + public Builder methodHandler(MethodHandler methodHandler) { this.methodHandler = methodHandler; return this; } @@ -387,13 +393,13 @@ public Rules callCredentials(CallCredentials callCredentials) { } /** - * Builds and returns a new instance of {@link io.helidon.webclient.grpc.ClientMethodDescriptor}. + * Builds and returns a new instance of {@link GrpcClientMethodDescriptor}. * - * @return a new instance of {@link io.helidon.webclient.grpc.ClientMethodDescriptor} + * @return a new instance of {@link GrpcClientMethodDescriptor} */ @Override @SuppressWarnings("unchecked") - public ClientMethodDescriptor build() { + public GrpcClientMethodDescriptor build() { MarshallerSupplier supplier = this.marshallerSupplier; if (supplier == null) { @@ -401,19 +407,18 @@ public ClientMethodDescriptor build() { } if (requestType != null) { - descriptor.setRequestMarshaller(supplier.get(requestType)); + descriptor.setRequestMarshaller((MethodDescriptor.Marshaller) supplier.get(requestType)); } if (responseType != null) { - descriptor.setResponseMarshaller(supplier.get(responseType)); + descriptor.setResponseMarshaller((MethodDescriptor.Marshaller) supplier.get(responseType)); } - return new ClientMethodDescriptor(name, + return new GrpcClientMethodDescriptor(name, descriptor.build(), interceptors.stream().toList(), callCredentials, methodHandler); } - } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java index f3248d2ba88..84e96b1191d 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java @@ -29,6 +29,6 @@ static GrpcClientStream create(SocketContext scoketContext, int streamId, StreamFlowControl flowControl, Http2StreamState streamState) { - return new GrpcClientStream(); + return null; } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java index 1bac6c80f36..0da4f3171f3 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java @@ -16,60 +16,22 @@ package io.helidon.webclient.grpc; -import io.helidon.common.buffers.BufferData; -import io.helidon.http.http2.Http2FrameHeader; -import io.helidon.http.http2.Http2Headers; -import io.helidon.http.http2.Http2Priority; -import io.helidon.http.http2.Http2RstStream; -import io.helidon.http.http2.Http2Stream; -import io.helidon.http.http2.Http2StreamState; -import io.helidon.http.http2.Http2WindowUpdate; -import io.helidon.http.http2.StreamFlowControl; -import io.helidon.webclient.api.ReleasableResource; - -class GrpcClientStream implements Http2Stream, ReleasableResource { - @Override - public boolean rstStream(Http2RstStream rstStream) { - return false; - } - - @Override - public void windowUpdate(Http2WindowUpdate windowUpdate) { - - } - - @Override - public void headers(Http2Headers headers, boolean endOfStream) { - - } - - @Override - public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) { - - } - - @Override - public void priority(Http2Priority http2Priority) { - - } - - @Override - public int streamId() { - return 0; - } - - @Override - public Http2StreamState streamState() { - return null; - } - - @Override - public StreamFlowControl flowControl() { - return null; - } - - @Override - public void closeResource() { - +import io.helidon.common.socket.SocketContext; +import io.helidon.http.http2.Http2Settings; +import io.helidon.webclient.http2.Http2ClientConfig; +import io.helidon.webclient.http2.Http2ClientConnection; +import io.helidon.webclient.http2.Http2ClientStream; +import io.helidon.webclient.http2.Http2StreamConfig; +import io.helidon.webclient.http2.LockingStreamIdSequence; + +class GrpcClientStream extends Http2ClientStream { + + GrpcClientStream(Http2ClientConnection connection, + Http2Settings serverSettings, + SocketContext ctx, + Http2StreamConfig http2StreamConfig, + Http2ClientConfig http2ClientConfig, + LockingStreamIdSequence streamIdSeq) { + super(connection, serverSettings, ctx, http2StreamConfig, http2ClientConfig, streamIdSeq); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java index b0555ac0c49..3c63f56ae8e 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java @@ -30,7 +30,7 @@ public GrpcProtocolProvider() { @Override public String protocolId() { - return "grpc"; + return CONFIG_KEY; } @Override diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index 7a9ba194c27..9655c8755c2 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -19,7 +19,6 @@ import java.util.Collection; import io.grpc.stub.StreamObserver; -import io.helidon.webclient.http2.Http2Client; import io.grpc.ClientCall; import io.grpc.MethodDescriptor; @@ -27,16 +26,16 @@ class GrpcServiceClientImpl implements GrpcServiceClient { private final GrpcServiceDescriptor descriptor; - private final Http2Client http2Client; + private final GrpcClientImpl grpcClient; - GrpcServiceClientImpl(GrpcServiceDescriptor descriptor, Http2Client http2Client) { + GrpcServiceClientImpl(GrpcServiceDescriptor descriptor, GrpcClientImpl grpcClient) { this.descriptor = descriptor; - this.http2Client = http2Client; + this.grpcClient = grpcClient; } @Override public String serviceName() { - return null; + return descriptor.serviceName(); } @Override @@ -57,7 +56,6 @@ public Collection serverStream(String methodName, ReqT requ @Override public void serverStream(String methodName, ReqT request, StreamObserver responseObserver) { - } @Override @@ -81,17 +79,14 @@ public StreamObserver bidi(String methodName, StreamObserver } private ClientCall ensureMethod(String methodName, MethodDescriptor.MethodType methodType) { - ClientMethodDescriptor method = descriptor.method(methodName); - + GrpcClientMethodDescriptor method = descriptor.method(methodName); if (!method.type().equals(methodType)) { throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + ", yet " + methodType + " was requested."); } - return createClientCall(method); } - private ClientCall createClientCall(ClientMethodDescriptor method) { - - return new GrpcClientCall<>(http2Client, method); + private ClientCall createClientCall(GrpcClientMethodDescriptor method) { + return new GrpcClientCall<>(grpcClient, method); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java similarity index 61% rename from webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java rename to webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java index f96b71a766a..00fff468699 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java @@ -13,30 +13,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.webclient.grpc; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import io.grpc.CallCredentials; import io.grpc.ClientInterceptor; +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; -/** - * All required meta-data about a client side gRPC service. - */ -public class GrpcServiceDescriptor { - private String serviceName; - private Map methods; - private List interceptors; - private CallCredentials callCredentials; - - ClientMethodDescriptor method(String name) { - ClientMethodDescriptor clientMethodDescriptor = methods.get(name); - if (clientMethodDescriptor == null) { +@Prototype.Blueprint +interface GrpcServiceDescriptorBlueprint { + + String serviceName(); + + @Option.Singular + Map methods(); + + default GrpcClientMethodDescriptor method(String name) { + GrpcClientMethodDescriptor descriptor = methods().get(name); + if (descriptor == null) { throw new NoSuchElementException("There is no method " + name + " defined for service " + this); } - return clientMethodDescriptor; + return descriptor; } + + @Option.Singular + List interceptors(); + + Optional callCredentials(); } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java index 71a2ff21139..ff114a795ac 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java @@ -61,7 +61,7 @@ import static java.lang.System.Logger.Level.TRACE; import static java.lang.System.Logger.Level.WARNING; -class Http2ClientConnection { +public class Http2ClientConnection { private static final System.Logger LOGGER = System.getLogger(Http2ClientConnection.class.getName()); private static final int FRAME_HEADER_LENGTH = 9; private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); @@ -103,7 +103,7 @@ class Http2ClientConnection { this.writer = new Http2ConnectionWriter(connection.helidonSocket(), connection.writer(), List.of()); } - static Http2ClientConnection create(Http2ClientImpl http2Client, + public static Http2ClientConnection create(Http2ClientImpl http2Client, ClientConnection connection, boolean sendSettings) { @@ -136,6 +136,10 @@ Http2ClientStream stream(int streamId) { } + public LockingStreamIdSequence streamIdSequence() { + return streamIdSeq; + } + Http2ClientStream createStream(Http2StreamConfig config) { //FIXME: priority Http2ClientStream stream = new Http2ClientStream(this, @@ -147,7 +151,7 @@ Http2ClientStream createStream(Http2StreamConfig config) { return stream; } - void addStream(int streamId, Http2ClientStream stream) { + public void addStream(int streamId, Http2ClientStream stream) { Lock lock = streamsLock.writeLock(); lock.lock(); try { @@ -157,7 +161,7 @@ void addStream(int streamId, Http2ClientStream stream) { } } - void removeStream(int streamId) { + public void removeStream(int streamId) { Lock lock = streamsLock.writeLock(); lock.lock(); try { diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java index 15715d53953..c45c66f44e1 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java @@ -27,7 +27,7 @@ import io.helidon.webclient.api.WebClient; import io.helidon.webclient.spi.HttpClientSpi; -class Http2ClientImpl implements Http2Client, HttpClientSpi { +public class Http2ClientImpl implements Http2Client, HttpClientSpi { private final WebClient webClient; private final Http2ClientConfig clientConfig; private final Http2ClientProtocolConfig protocolConfig; diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index 46c82fa33fe..a71f88faae6 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -53,7 +53,7 @@ import static java.lang.System.Logger.Level.DEBUG; -class Http2ClientStream implements Http2Stream, ReleasableResource { +public class Http2ClientStream implements Http2Stream, ReleasableResource { private static final System.Logger LOGGER = System.getLogger(Http2ClientStream.class.getName()); private static final Set NON_CANCELABLE = Set.of(Http2StreamState.CLOSED, Http2StreamState.IDLE); @@ -80,7 +80,7 @@ class Http2ClientStream implements Http2Stream, ReleasableResource { private int streamId; private StreamBuffer buffer; - Http2ClientStream(Http2ClientConnection connection, + protected Http2ClientStream(Http2ClientConnection connection, Http2Settings serverSettings, SocketContext ctx, Http2StreamConfig http2StreamConfig, @@ -111,6 +111,7 @@ public void headers(Http2Headers headers, boolean endOfStream) { this.currentHeaders = headers; this.hasEntity = !endOfStream; } + @Override public boolean rstStream(Http2RstStream rstStream) { if (state == Http2StreamState.IDLE) { diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java index 96decead36f..f50d1e36758 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java @@ -29,7 +29,7 @@ import io.helidon.webclient.http1.Http1ClientResponse; import io.helidon.webclient.spi.ClientConnectionCache; -final class Http2ConnectionCache extends ClientConnectionCache { +public final class Http2ConnectionCache extends ClientConnectionCache { private static final Http2ConnectionCache SHARED = new Http2ConnectionCache(true); private final LruCache http2Supported = LruCache.builder() .capacity(1000) @@ -41,11 +41,11 @@ private Http2ConnectionCache(boolean shared) { super(shared); } - static Http2ConnectionCache shared() { + public static Http2ConnectionCache shared() { return SHARED; } - static Http2ConnectionCache create() { + public static Http2ConnectionCache create() { return new Http2ConnectionCache(false); } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java index 1e9e6cbd1b7..29bc30b5032 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java @@ -18,7 +18,7 @@ import java.time.Duration; -interface Http2StreamConfig { +public interface Http2StreamConfig { boolean priorKnowledge(); int priority(); diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java index 51b605286e5..6df24cc34c9 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java @@ -19,17 +19,17 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -class LockingStreamIdSequence { +public class LockingStreamIdSequence { private final AtomicInteger streamIdSeq = new AtomicInteger(0); private final Lock lock = new ReentrantLock(); int lockAndNext() { - lock.lock(); - return streamIdSeq.updateAndGet(o -> o % 2 == 0 ? o + 1 : o + 2); + lock.lock(); + return streamIdSeq.updateAndGet(o -> o % 2 == 0 ? o + 1 : o + 2); } - void unlock(){ + void unlock() { lock.unlock(); } } From 9e89838903003bef7fe8b5f72b9b10dfddb6c698 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 7 Feb 2024 11:39:19 -0500 Subject: [PATCH 03/38] Simple unary method call without using stubs. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/GrpcTest.java | 11 +- .../webclient/grpc/GrpcClientCall.java | 153 ++++++++++++------ .../webclient/grpc/GrpcClientStream.java | 23 +++ .../http2/Http2ClientConnection.java | 2 +- .../webclient/http2/Http2ClientStream.java | 14 +- .../java/io/helidon/webserver/RouterImpl.java | 2 +- 6 files changed, 148 insertions(+), 57 deletions(-) diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java index 56e05ca422a..2a77c407df6 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java @@ -27,7 +27,10 @@ import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.ServerTest; import io.helidon.webserver.testing.junit5.SetUpServer; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.RepeatedTest; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; @ServerTest class GrpcTest { @@ -53,7 +56,7 @@ private static Strings.StringMessage blockingGrpcUpper(Strings.StringMessage req .build(); } - @Test + @RepeatedTest(3) void testSimpleCall() { GrpcServiceDescriptor serviceDescriptor = GrpcServiceDescriptor.builder() @@ -65,7 +68,9 @@ void testSimpleCall() { .build()) .build(); - String r = grpcClient.serviceClient(serviceDescriptor) + StringValue r = grpcClient.serviceClient(serviceDescriptor) .unary("Upper", StringValue.of("hello")); + System.out.println("r = " + r.getValue()); + assertThat(r.getValue(), is("HELLO")); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 8e4cc327c4e..3cf060f55f2 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -16,22 +16,32 @@ package io.helidon.webclient.grpc; +import java.io.InputStream; import java.time.Duration; import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import io.grpc.ClientCall; import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.helidon.common.buffers.BufferData; +import io.helidon.common.tls.Tls; import io.helidon.http.Header; import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; +import io.helidon.http.WritableHeaders; +import io.helidon.http.http2.Http2Headers; import io.helidon.http.http2.Http2Settings; import io.helidon.webclient.api.ClientConnection; import io.helidon.webclient.api.ClientUri; import io.helidon.webclient.api.ConnectionKey; import io.helidon.webclient.api.DefaultDnsResolver; import io.helidon.webclient.api.DnsAddressLookup; +import io.helidon.webclient.api.Proxy; import io.helidon.webclient.api.TcpClientConnection; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.http2.Http2ClientConnection; @@ -41,81 +51,132 @@ /** * A gRPC client call handler. The typical order of calls will be: * - * start request* sendMessage* halfClose + * start request* sendMessage* halfClose + * + * TODO: memory synchronization across method calls * * @param * @param */ class GrpcClientCall extends ClientCall { + private static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); - private final AtomicReference> responseListener = new AtomicReference<>(); private final GrpcClientImpl grpcClient; private final GrpcClientMethodDescriptor method; private final AtomicInteger messages = new AtomicInteger(); + private final MethodDescriptor.Marshaller requestMarshaller; + private final MethodDescriptor.Marshaller responseMarshaller; + private final Queue messageQueue = new LinkedBlockingQueue<>(); + + private volatile Http2ClientConnection connection; + private volatile GrpcClientStream clientStream; + private volatile Listener responseListener; + + @SuppressWarnings("unchecked") GrpcClientCall(GrpcClientImpl grpcClient, GrpcClientMethodDescriptor method) { this.grpcClient = grpcClient; this.method = method; + this.requestMarshaller = (MethodDescriptor.Marshaller) method.descriptor().getRequestMarshaller(); + this.responseMarshaller = (MethodDescriptor.Marshaller) method.descriptor().getResponseMarshaller(); } @Override - public void start(Listener responseListener, Metadata headers) { - if (this.responseListener.compareAndSet(null, responseListener)) { - // obtain HTTP2 connection - Http2ClientConnection connection = Http2ClientConnection.create( - (Http2ClientImpl) grpcClient.http2Client(), clientConnection(), true); - - // create HTTP2 stream from connection - GrpcClientStream clientStream = new GrpcClientStream( - connection, - Http2Settings.create(), // Http2Settings - null, // SocketContext - new Http2StreamConfig() { - @Override - public boolean priorKnowledge() { - return true; - } - - @Override - public int priority() { - return 0; - } - - @Override - public Duration readTimeout() { - return grpcClient.prototype().readTimeout().orElse(null); - } - }, - null, // Http2ClientConfig - connection.streamIdSequence()); - - // send HEADERS frame - } else { - throw new IllegalStateException("Response listener was already set"); - } + public void start(Listener responseListener, Metadata metadata) { + this.responseListener = responseListener; + + // obtain HTTP2 connection + connection = Http2ClientConnection.create((Http2ClientImpl) grpcClient.http2Client(), + clientConnection(), true); + + // create HTTP2 stream from connection + clientStream = new GrpcClientStream( + connection, + Http2Settings.create(), // Http2Settings + null, // SocketContext + new Http2StreamConfig() { + @Override + public boolean priorKnowledge() { + return true; + } + + @Override + public int priority() { + return 0; + } + + @Override + public Duration readTimeout() { + return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(60)); + } + }, + null, // Http2ClientConfig + connection.streamIdSequence()); + + // send HEADERS frame + ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); + WritableHeaders headers = WritableHeaders.create(); + headers.add(Http2Headers.AUTHORITY_NAME, clientUri.authority()); + headers.add(Http2Headers.METHOD_NAME, "POST"); + headers.add(Http2Headers.PATH_NAME, "/" + method.descriptor().getFullMethodName()); + headers.add(Http2Headers.SCHEME_NAME, "http"); + headers.add(GRPC_CONTENT_TYPE); + headers.add(GRPC_ACCEPT_ENCODING); + clientStream.writeHeaders(Http2Headers.create(headers), false); } @Override public void request(int numMessages) { messages.addAndGet(numMessages); + + ExecutorService executor = grpcClient.webClient().executor(); + executor.submit(() -> { + clientStream.readHeaders(); + while (messages.decrementAndGet() > 0) { + BufferData bufferData = clientStream.read(); + bufferData.read(); // compression + bufferData.readUnsignedInt32(); // length prefixed + ResT res = responseMarshaller.parse(new InputStream() { + @Override + public int read() { + return bufferData.available() > 0 ? bufferData.read() : -1; + } + }); + responseListener.onMessage(res); + } + responseListener.onClose(Status.OK, new Metadata()); + clientStream.close(); + connection.close(); + }); } @Override public void cancel(String message, Throwable cause) { // close the stream/connection via RST_STREAM - // can be closed even if halfClosed + messageQueue.clear(); + clientStream.cancel(); + connection.close(); } @Override public void halfClose() { - // close the stream/connection - // GOAWAY frame + // drain the message queue + while (!messageQueue.isEmpty()) { + BufferData msg = messageQueue.poll(); + clientStream.writeData(msg, messageQueue.isEmpty()); + } } @Override public void sendMessage(ReqT message) { - // send a DATA frame + // queue a message + BufferData messageData = BufferData.growing(512); + messageData.readFrom(requestMarshaller.stream(message)); + BufferData headerData = BufferData.create(5); + headerData.writeInt8(0); // no compression + headerData.writeUnsignedInt32(messageData.available()); // length prefixed + messageQueue.add(BufferData.create(headerData, messageData)); } private ClientConnection clientConnection() { @@ -123,20 +184,22 @@ private ClientConnection clientConnection() { ClientUri clientUri = clientConfig.baseUri().orElseThrow(); WebClient webClient = grpcClient.webClient(); + Tls tls = Tls.builder().enabled(false).build(); ConnectionKey connectionKey = new ConnectionKey( clientUri.scheme(), clientUri.host(), clientUri.port(), - clientConfig.readTimeout().orElse(null), - null, + clientConfig.readTimeout().orElse(Duration.ZERO), + tls, DefaultDnsResolver.create(), DnsAddressLookup.defaultLookup(), - null); + Proxy.noProxy()); return TcpClientConnection.create(webClient, connectionKey, Collections.emptyList(), connection -> false, - connection -> {}).connect(); + connection -> { + }).connect(); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java index 0da4f3171f3..d4a4d60e09e 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java @@ -16,7 +16,10 @@ package io.helidon.webclient.grpc; +import io.helidon.common.buffers.BufferData; import io.helidon.common.socket.SocketContext; +import io.helidon.http.http2.Http2FrameHeader; +import io.helidon.http.http2.Http2Headers; import io.helidon.http.http2.Http2Settings; import io.helidon.webclient.http2.Http2ClientConfig; import io.helidon.webclient.http2.Http2ClientConnection; @@ -34,4 +37,24 @@ class GrpcClientStream extends Http2ClientStream { LockingStreamIdSequence streamIdSeq) { super(connection, serverSettings, ctx, http2StreamConfig, http2ClientConfig, streamIdSeq); } + + @Override + public void headers(Http2Headers headers, boolean endOfStream) { + super.headers(headers, endOfStream); + } + + @Override + public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) { + super.data(header, data, endOfStream); + } + + @Override + public void cancel() { + super.cancel(); + } + + @Override + public void close() { + super.close(); + } } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java index ff114a795ac..32cb9b903f9 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java @@ -206,7 +206,7 @@ void updateLastStreamId(int lastStreamId) { this.lastStreamId = lastStreamId; } - void close() { + public void close() { this.goAway(0, Http2ErrorCode.NO_ERROR, "Closing connection"); if (state.getAndSet(State.CLOSED) != State.CLOSED) { try { diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index a71f88faae6..e52b58b69cf 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -190,7 +190,7 @@ boolean hasEntity() { return hasEntity; } - void cancel() { + public void cancel() { if (NON_CANCELABLE.contains(state)) { return; } @@ -206,7 +206,7 @@ void cancel() { } } - void close() { + public void close() { connection.removeStream(streamId); } @@ -227,7 +227,7 @@ BufferData read(int i) { return read(); } - BufferData read() { + public BufferData read() { while (state == Http2StreamState.HALF_CLOSED_LOCAL && readState != ReadState.END && hasEntity) { Http2FrameData frameData = readOne(timeout); if (frameData != null) { @@ -258,7 +258,7 @@ Status waitFor100Continue() { return null; } - void writeHeaders(Http2Headers http2Headers, boolean endOfStream) { + public void writeHeaders(Http2Headers http2Headers, boolean endOfStream) { this.state = Http2StreamState.checkAndGetState(this.state, Http2FrameType.HEADERS, true, endOfStream, true); this.readState = readState.check(http2Headers.httpHeaders().contains(HeaderValues.EXPECT_100) ? ReadState.CONTINUE_100_HEADERS @@ -294,7 +294,7 @@ void writeHeaders(Http2Headers http2Headers, boolean endOfStream) { } } - void writeData(BufferData entityBytes, boolean endOfStream) { + public void writeData(BufferData entityBytes, boolean endOfStream) { Http2FrameHeader frameHeader = Http2FrameHeader.create(entityBytes.available(), Http2FrameTypes.DATA, Http2Flag.DataFlags.create(endOfStream @@ -305,7 +305,7 @@ void writeData(BufferData entityBytes, boolean endOfStream) { splitAndWrite(frameData); } - Http2Headers readHeaders() { + public Http2Headers readHeaders() { while (readState == ReadState.HEADERS) { Http2FrameData frameData = readOne(timeout); if (frameData != null) { @@ -315,7 +315,7 @@ Http2Headers readHeaders() { return currentHeaders; } - SocketContext ctx() { + public SocketContext ctx() { return ctx; } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java b/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java index c0b5df7020e..026c652366d 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java @@ -84,7 +84,7 @@ public RouterImpl build() { public Router.Builder addRouting(io.helidon.common.Builder routing) { var previous = this.routings.put(routing.getClass(), routing); if (previous != null) { - Thread.dumpStack(); + // Thread.dumpStack(); LOGGER.log(System.Logger.Level.WARNING, "Second routing of the same type is registered. " + "The first instance will be ignored. Type: " + routing.getClass().getName()); } From d20a820d0907302c0e122a86e5c74a49676d5d4e Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 9 Feb 2024 10:10:45 -0500 Subject: [PATCH 04/38] Basic support for unary, serverStream, streamClient and bidi invocations. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/GrpcTest.java | 151 ++++++++++++++- .../webclient/grpc/GrpcClientCall.java | 176 ++++++++++++++---- .../webclient/grpc/GrpcServiceClient.java | 16 +- .../webclient/grpc/GrpcServiceClientImpl.java | 102 ++++++++-- .../webclient/http2/Http2ClientStream.java | 4 +- 5 files changed, 374 insertions(+), 75 deletions(-) diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java index 2a77c407df6..3bf1d89a80f 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java @@ -16,9 +16,12 @@ package io.helidon.examples.webserver.protocols; +import java.util.Iterator; +import java.util.List; import java.util.Locale; import com.google.protobuf.StringValue; +import io.grpc.stub.StreamObserver; import io.helidon.examples.grpc.strings.Strings; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; @@ -27,7 +30,7 @@ import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.ServerTest; import io.helidon.webserver.testing.junit5.SetUpServer; -import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -44,20 +47,94 @@ private GrpcTest(GrpcClient grpcClient) { @SetUpServer public static void setup(WebServerConfig.Builder builder) { builder.addRouting(GrpcRouting.builder() - .unary(Strings.getDescriptor(), - "StringService", - "Upper", - GrpcTest::blockingGrpcUpper)); + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + GrpcTest::upper) + .serverStream(Strings.getDescriptor(), + "StringService", + "Split", + GrpcTest::split) + .clientStream(Strings.getDescriptor(), + "StringService", + "Join", + GrpcTest::join) + .bidi(Strings.getDescriptor(), + "StringService", + "Echo", + GrpcTest::echo)); } - private static Strings.StringMessage blockingGrpcUpper(Strings.StringMessage reqT) { + private static Strings.StringMessage upper(Strings.StringMessage reqT) { return Strings.StringMessage.newBuilder() .setText(reqT.getText().toUpperCase(Locale.ROOT)) .build(); } - @RepeatedTest(3) - void testSimpleCall() { + private static void split(Strings.StringMessage reqT, + StreamObserver streamObserver) { + String[] strings = reqT.getText().split(" "); + for (String string : strings) { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(string) + .build()); + + } + streamObserver.onCompleted(); + } + + private static StreamObserver join(StreamObserver streamObserver) { + return new StreamObserver<>() { + private StringBuilder builder; + + @Override + public void onNext(Strings.StringMessage value) { + if (builder == null) { + builder = new StringBuilder(); + builder.append(value.getText()); + } else { + builder.append(" ").append(value.getText()); + } + } + + @Override + public void onError(Throwable t) { + streamObserver.onError(t); + } + + @Override + public void onCompleted() { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(builder.toString()) + .build()); + streamObserver.onCompleted(); + } + }; + } + + private static StreamObserver echo(StreamObserver streamObserver) { + return new StreamObserver<>() { + private StringBuilder builder; + + @Override + public void onNext(Strings.StringMessage value) { + streamObserver.onNext(value); + } + + @Override + public void onError(Throwable t) { + streamObserver.onError(t); + } + + @Override + public void onCompleted() { + streamObserver.onCompleted(); + } + }; + } + + @Test + void testUnaryUpper() { GrpcServiceDescriptor serviceDescriptor = GrpcServiceDescriptor.builder() .serviceName("StringService") @@ -70,7 +147,63 @@ void testSimpleCall() { StringValue r = grpcClient.serviceClient(serviceDescriptor) .unary("Upper", StringValue.of("hello")); - System.out.println("r = " + r.getValue()); assertThat(r.getValue(), is("HELLO")); } + + @Test + void testServerStreamingSplit() { + GrpcServiceDescriptor serviceDescriptor = + GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Split", + GrpcClientMethodDescriptor.serverStreaming("StringService", "Split") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .build(); + + Iterator r = grpcClient.serviceClient(serviceDescriptor) + .serverStream("Split", StringValue.of("hello world")); + assertThat(r.next().getValue(), is("hello")); + assertThat(r.next().getValue(), is("world")); + assertThat(r.hasNext(), is(false)); + } + + @Test + void testClientStreamingJoin() { + GrpcServiceDescriptor serviceDescriptor = + GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Join", + GrpcClientMethodDescriptor.clientStreaming("StringService", "Join") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .build(); + + StringValue r = grpcClient.serviceClient(serviceDescriptor) + .clientStream("Join", List.of(StringValue.of("hello"), + StringValue.of("world")).iterator()); + assertThat(r.getValue(), is("hello world")); + } + + @Test + void testBidirectionalEcho() { + GrpcServiceDescriptor serviceDescriptor = + GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Echo", + GrpcClientMethodDescriptor.bidirectional("StringService", "Echo") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .build(); + + Iterator r = grpcClient.serviceClient(serviceDescriptor) + .bidi("Echo", List.of(StringValue.of("hello"), + StringValue.of("world")).iterator()); + assertThat(r.next().getValue(), is("hello")); + assertThat(r.next().getValue(), is("world")); + assertThat(r.hasNext(), is(false)); + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 3cf060f55f2..dc9a37aeaa4 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -19,9 +19,11 @@ import java.io.InputStream; import java.time.Duration; import java.util.Collections; -import java.util.Queue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import io.grpc.ClientCall; @@ -34,8 +36,10 @@ import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; import io.helidon.http.WritableHeaders; +import io.helidon.http.http2.Http2FrameData; import io.helidon.http.http2.Http2Headers; import io.helidon.http.http2.Http2Settings; +import io.helidon.http.http2.Http2StreamState; import io.helidon.webclient.api.ClientConnection; import io.helidon.webclient.api.ClientUri; import io.helidon.webclient.api.ConnectionKey; @@ -47,32 +51,48 @@ import io.helidon.webclient.http2.Http2ClientConnection; import io.helidon.webclient.http2.Http2ClientImpl; import io.helidon.webclient.http2.Http2StreamConfig; +import io.helidon.webclient.http2.StreamTimeoutException; + +import static java.lang.System.Logger.Level.DEBUG; /** * A gRPC client call handler. The typical order of calls will be: * - * start request* sendMessage* halfClose - * - * TODO: memory synchronization across method calls + * start (request | sendMessage)* (halfClose | cancel) * * @param * @param */ class GrpcClientCall extends ClientCall { + private static final System.Logger LOGGER = System.getLogger(GrpcClientCall.class.getName()); + private static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); + private static final int WAIT_TIME_MILLIS = 100; + private static final Duration WAIT_TIME_MILLIS_DURATION = Duration.ofMillis(WAIT_TIME_MILLIS); + + private static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); + + private final ExecutorService executor; private final GrpcClientImpl grpcClient; private final GrpcClientMethodDescriptor method; - private final AtomicInteger messages = new AtomicInteger(); + private final AtomicInteger messageRequest = new AtomicInteger(); private final MethodDescriptor.Marshaller requestMarshaller; private final MethodDescriptor.Marshaller responseMarshaller; - private final Queue messageQueue = new LinkedBlockingQueue<>(); + + private final LinkedBlockingQueue sendingQueue = new LinkedBlockingQueue<>(); + private final LinkedBlockingQueue receivingQueue = new LinkedBlockingQueue<>(); + + private final CountDownLatch startReadBarrier = new CountDownLatch(1); + private final CountDownLatch startWriteBarrier = new CountDownLatch(1); private volatile Http2ClientConnection connection; private volatile GrpcClientStream clientStream; private volatile Listener responseListener; + private volatile Future readStreamFuture; + private volatile Future writeStreamFuture; @SuppressWarnings("unchecked") GrpcClientCall(GrpcClientImpl grpcClient, GrpcClientMethodDescriptor method) { @@ -80,6 +100,7 @@ class GrpcClientCall extends ClientCall { this.method = method; this.requestMarshaller = (MethodDescriptor.Marshaller) method.descriptor().getRequestMarshaller(); this.responseMarshaller = (MethodDescriptor.Marshaller) method.descriptor().getResponseMarshaller(); + this.executor = grpcClient.webClient().executor(); } @Override @@ -114,6 +135,9 @@ public Duration readTimeout() { null, // Http2ClientConfig connection.streamIdSequence()); + // start streaming threads + startStreamingThreads(); + // send HEADERS frame ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); WritableHeaders headers = WritableHeaders.create(); @@ -128,44 +152,20 @@ public Duration readTimeout() { @Override public void request(int numMessages) { - messages.addAndGet(numMessages); - - ExecutorService executor = grpcClient.webClient().executor(); - executor.submit(() -> { - clientStream.readHeaders(); - while (messages.decrementAndGet() > 0) { - BufferData bufferData = clientStream.read(); - bufferData.read(); // compression - bufferData.readUnsignedInt32(); // length prefixed - ResT res = responseMarshaller.parse(new InputStream() { - @Override - public int read() { - return bufferData.available() > 0 ? bufferData.read() : -1; - } - }); - responseListener.onMessage(res); - } - responseListener.onClose(Status.OK, new Metadata()); - clientStream.close(); - connection.close(); - }); + messageRequest.addAndGet(numMessages); + LOGGER.log(DEBUG, () -> "Messages requested " + numMessages); + startReadBarrier.countDown(); } @Override public void cancel(String message, Throwable cause) { - // close the stream/connection via RST_STREAM - messageQueue.clear(); - clientStream.cancel(); - connection.close(); + responseListener.onClose(Status.CANCELLED, new Metadata()); + close(); } @Override public void halfClose() { - // drain the message queue - while (!messageQueue.isEmpty()) { - BufferData msg = messageQueue.poll(); - clientStream.writeData(msg, messageQueue.isEmpty()); - } + sendingQueue.add(EMPTY_BUFFER_DATA); // end marker } @Override @@ -176,7 +176,84 @@ public void sendMessage(ReqT message) { BufferData headerData = BufferData.create(5); headerData.writeInt8(0); // no compression headerData.writeUnsignedInt32(messageData.available()); // length prefixed - messageQueue.add(BufferData.create(headerData, messageData)); + sendingQueue.add(BufferData.create(headerData, messageData)); + startWriteBarrier.countDown(); + } + + private void startStreamingThreads() { + // write streaming thread + writeStreamFuture = executor.submit(() -> { + try { + startWriteBarrier.await(); + LOGGER.log(DEBUG, "[Writing thread] started"); + + while (isRemoteOpen()) { + LOGGER.log(DEBUG, "[Writing thread] polling sending queue"); + BufferData bufferData = sendingQueue.poll(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); + if (bufferData != null) { + if (bufferData == EMPTY_BUFFER_DATA) { // end marker + LOGGER.log(DEBUG, "[Writing thread] sending queue end marker found"); + break; + } + boolean endOfStream = (sendingQueue.peek() == EMPTY_BUFFER_DATA); + LOGGER.log(DEBUG, () -> "[Writing thread] writing bufferData " + endOfStream); + clientStream.writeData(bufferData, endOfStream); + } + } + } catch (InterruptedException e) { + // falls through + } + LOGGER.log(DEBUG, "[Writing thread] exiting"); + }); + + // read streaming thread + readStreamFuture = executor.submit(() -> { + try { + startReadBarrier.await(); + LOGGER.log(DEBUG, "[Reading thread] started"); + + // read response headers + clientStream.readHeaders(); + + while (isRemoteOpen()) { + // attempt to send queued messages + drainReceivingQueue(); + + // attempt to read and queue + Http2FrameData frameData; + try { + frameData = clientStream.readOne(WAIT_TIME_MILLIS_DURATION); + } catch (StreamTimeoutException e) { + LOGGER.log(DEBUG, "[Reading thread] read timeout"); + continue; + } + if (frameData != null) { + receivingQueue.add(frameData.data()); + LOGGER.log(DEBUG, "[Reading thread] adding bufferData to receiving queue"); + } + + // trailers received? + if (clientStream.trailers().isDone()) { + drainReceivingQueue(); // one more attempt + break; + } + } + + responseListener.onClose(Status.OK, new Metadata()); + close(); + } catch (InterruptedException e) { + // falls through + } + LOGGER.log(DEBUG, "[Reading thread] exiting"); + }); + } + + private void close() { + readStreamFuture.cancel(true); + writeStreamFuture.cancel(true); + sendingQueue.clear(); + clientStream.cancel(); + connection.close(); } private ClientConnection clientConnection() { @@ -202,4 +279,29 @@ private ClientConnection clientConnection() { connection -> { }).connect(); } + + private boolean isRemoteOpen() { + return clientStream.streamState() != Http2StreamState.HALF_CLOSED_REMOTE + && clientStream.streamState() != Http2StreamState.CLOSED; + } + + private ResT toResponse(BufferData bufferData) { + bufferData.read(); // compression + bufferData.readUnsignedInt32(); // length prefixed + return responseMarshaller.parse(new InputStream() { + @Override + public int read() { + return bufferData.available() > 0 ? bufferData.read() : -1; + } + }); + } + + private void drainReceivingQueue() { + while (messageRequest.get() > 0 && !receivingQueue.isEmpty()) { + messageRequest.getAndDecrement(); + ResT res = toResponse(receivingQueue.remove()); + LOGGER.log(DEBUG, "[Reading thread] sending response to listener"); + responseListener.onMessage(res); + } + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java index 0a578892e78..b5f54bdd5ed 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java @@ -16,7 +16,7 @@ package io.helidon.webclient.grpc; -import java.util.Collection; +import java.util.Iterator; import io.grpc.stub.StreamObserver; @@ -35,17 +35,17 @@ public interface GrpcServiceClient { RespT unary(String methodName, ReqT request); - StreamObserver unary(String methodName, StreamObserver responseObserver); + StreamObserver unary(String methodName, StreamObserver response); - Collection serverStream(String methodName, ReqT request); + Iterator serverStream(String methodName, ReqT request); - void serverStream(String methodName, ReqT request, StreamObserver responseObserver); + void serverStream(String methodName, ReqT request, StreamObserver response); - RespT clientStream(String methodName, Collection request); + RespT clientStream(String methodName, Iterator request); - StreamObserver clientStream(String methodName, StreamObserver responseObserver); + StreamObserver clientStream(String methodName, StreamObserver response); - Collection bidi(String methodName, Collection responseObserver); + Iterator bidi(String methodName, Iterator request); - StreamObserver bidi(String methodName, StreamObserver responseObserver); + StreamObserver bidi(String methodName, StreamObserver response); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index 9655c8755c2..34ac48f73bd 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -16,13 +16,15 @@ package io.helidon.webclient.grpc; -import java.util.Collection; - -import io.grpc.stub.StreamObserver; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; import io.grpc.ClientCall; import io.grpc.MethodDescriptor; import io.grpc.stub.ClientCalls; +import io.grpc.stub.StreamObserver; class GrpcServiceClientImpl implements GrpcServiceClient { private final GrpcServiceDescriptor descriptor; @@ -40,53 +42,115 @@ public String serviceName() { @Override public RespT unary(String methodName, ReqT request) { - ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.UNARY); + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.UNARY); return ClientCalls.blockingUnaryCall(call, request); } @Override - public StreamObserver unary(String methodName, StreamObserver responseObserver) { + public StreamObserver unary(String methodName, StreamObserver response) { return null; } @Override - public Collection serverStream(String methodName, ReqT request) { - return null; + public Iterator serverStream(String methodName, ReqT request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.SERVER_STREAMING); + return ClientCalls.blockingServerStreamingCall(call, request); } @Override - public void serverStream(String methodName, ReqT request, StreamObserver responseObserver) { + public void serverStream(String methodName, ReqT request, StreamObserver response) { } @Override - public RespT clientStream(String methodName, Collection request) { - return null; + public RespT clientStream(String methodName, Iterator request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.CLIENT_STREAMING); + CompletableFuture future = new CompletableFuture<>(); + StreamObserver observer = ClientCalls.asyncClientStreamingCall(call, new StreamObserver<>() { + private RespT value; + + @Override + public void onNext(RespT value) { + this.value = value; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value); + } + }); + + // send client stream + while (request.hasNext()) { + observer.onNext(request.next()); + } + observer.onCompleted(); + + // block waiting for response + try { + return future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override - public StreamObserver clientStream(String methodName, StreamObserver responseObserver) { + public StreamObserver clientStream(String methodName, StreamObserver response) { return null; } @Override - public Collection bidi(String methodName, Collection responseObserver) { - return null; + public Iterator bidi(String methodName, Iterator request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.BIDI_STREAMING); + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver observer = ClientCalls.asyncBidiStreamingCall(call, new StreamObserver<>() { + private final List values = new ArrayList<>(); + + @Override + public void onNext(RespT value) { + values.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(values.iterator()); + } + }); + + // send client stream + while (request.hasNext()) { + observer.onNext(request.next()); + } + observer.onCompleted(); + + // block waiting for response + try { + return future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override - public StreamObserver bidi(String methodName, StreamObserver responseObserver) { + public StreamObserver bidi(String methodName, StreamObserver response) { return null; } private ClientCall ensureMethod(String methodName, MethodDescriptor.MethodType methodType) { GrpcClientMethodDescriptor method = descriptor.method(methodName); if (!method.type().equals(methodType)) { - throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + ", yet " + methodType + " was requested."); + throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + + ", yet " + methodType + " was requested."); } - return createClientCall(method); - } - - private ClientCall createClientCall(GrpcClientMethodDescriptor method) { return new GrpcClientCall<>(grpcClient, method); } } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index e52b58b69cf..351d6558ce0 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -182,7 +182,7 @@ void trailers(Http2Headers headers, boolean endOfStream) { trailers.complete(headers.httpHeaders()); } - CompletableFuture trailers() { + public CompletableFuture trailers() { return trailers; } @@ -319,7 +319,7 @@ public SocketContext ctx() { return ctx; } - private Http2FrameData readOne(Duration pollTimeout) { + public Http2FrameData readOne(Duration pollTimeout) { Http2FrameData frameData = buffer.poll(pollTimeout); if (frameData != null) { From 907f81a076dbffd287a7bd261fd1083387ff1d50 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 12 Feb 2024 14:18:11 -0500 Subject: [PATCH 05/38] Support for sync and async gRPC calls. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/GrpcTest.java | 201 +++++++++++++----- .../webclient/grpc/GrpcServiceClient.java | 16 +- .../webclient/grpc/GrpcServiceClientImpl.java | 49 +++-- 3 files changed, 179 insertions(+), 87 deletions(-) diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java index 3bf1d89a80f..7e57fe5b1d8 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java @@ -16,9 +16,14 @@ package io.helidon.examples.webserver.protocols; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import com.google.protobuf.StringValue; import io.grpc.stub.StreamObserver; @@ -37,11 +42,36 @@ @ServerTest class GrpcTest { + private static final long TIMEOUT_SECONDS = 10; private final GrpcClient grpcClient; + private final GrpcServiceDescriptor serviceDescriptor; private GrpcTest(GrpcClient grpcClient) { this.grpcClient = grpcClient; + this.serviceDescriptor = GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Upper", + GrpcClientMethodDescriptor.unary("StringService", "Upper") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .putMethod("Split", + GrpcClientMethodDescriptor.serverStreaming("StringService", "Split") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .putMethod("Join", + GrpcClientMethodDescriptor.clientStreaming("StringService", "Join") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .putMethod("Echo", + GrpcClientMethodDescriptor.bidirectional("StringService", "Echo") + .requestType(StringValue.class) + .responseType(StringValue.class) + .build()) + .build(); } @SetUpServer @@ -65,6 +95,8 @@ public static void setup(WebServerConfig.Builder builder) { GrpcTest::echo)); } + // -- gRPC server methods -- + private static Strings.StringMessage upper(Strings.StringMessage reqT) { return Strings.StringMessage.newBuilder() .setText(reqT.getText().toUpperCase(Locale.ROOT)) @@ -114,8 +146,6 @@ public void onCompleted() { private static StreamObserver echo(StreamObserver streamObserver) { return new StreamObserver<>() { - private StringBuilder builder; - @Override public void onNext(Strings.StringMessage value) { streamObserver.onNext(value); @@ -133,77 +163,134 @@ public void onCompleted() { }; } + // -- Tests -- + @Test void testUnaryUpper() { - GrpcServiceDescriptor serviceDescriptor = - GrpcServiceDescriptor.builder() - .serviceName("StringService") - .putMethod("Upper", - GrpcClientMethodDescriptor.unary("StringService", "Upper") - .requestType(StringValue.class) - .responseType(StringValue.class) - .build()) - .build(); - - StringValue r = grpcClient.serviceClient(serviceDescriptor) + StringValue res = grpcClient.serviceClient(serviceDescriptor) .unary("Upper", StringValue.of("hello")); - assertThat(r.getValue(), is("HELLO")); + assertThat(res.getValue(), is("HELLO")); + } + + @Test + void testUnaryUpperAsync() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + grpcClient.serviceClient(serviceDescriptor) + .unary("Upper", + StringValue.of("hello"), + singleStreamObserver(future)); + StringValue res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getValue(), is("HELLO")); } @Test void testServerStreamingSplit() { - GrpcServiceDescriptor serviceDescriptor = - GrpcServiceDescriptor.builder() - .serviceName("StringService") - .putMethod("Split", - GrpcClientMethodDescriptor.serverStreaming("StringService", "Split") - .requestType(StringValue.class) - .responseType(StringValue.class) - .build()) - .build(); - - Iterator r = grpcClient.serviceClient(serviceDescriptor) - .serverStream("Split", StringValue.of("hello world")); - assertThat(r.next().getValue(), is("hello")); - assertThat(r.next().getValue(), is("world")); - assertThat(r.hasNext(), is(false)); + Iterator res = grpcClient.serviceClient(serviceDescriptor) + .serverStream("Split", + StringValue.of("hello world")); + assertThat(res.next().getValue(), is("hello")); + assertThat(res.next().getValue(), is("world")); + assertThat(res.hasNext(), is(false)); + } + + @Test + void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture> future = new CompletableFuture<>(); + grpcClient.serviceClient(serviceDescriptor) + .serverStream("Split", + StringValue.of("hello world"), + multiStreamObserver(future)); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getValue(), is("hello")); + assertThat(res.next().getValue(), is("world")); + assertThat(res.hasNext(), is(false)); } @Test void testClientStreamingJoin() { - GrpcServiceDescriptor serviceDescriptor = - GrpcServiceDescriptor.builder() - .serviceName("StringService") - .putMethod("Join", - GrpcClientMethodDescriptor.clientStreaming("StringService", "Join") - .requestType(StringValue.class) - .responseType(StringValue.class) - .build()) - .build(); - - StringValue r = grpcClient.serviceClient(serviceDescriptor) + StringValue res = grpcClient.serviceClient(serviceDescriptor) .clientStream("Join", List.of(StringValue.of("hello"), - StringValue.of("world")).iterator()); - assertThat(r.getValue(), is("hello world")); + StringValue.of("world")).iterator()); + assertThat(res.getValue(), is("hello world")); + } + + @Test + void testClientStreamingJoinAsync() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver req = grpcClient.serviceClient(serviceDescriptor) + .clientStream("Join", singleStreamObserver(future)); + req.onNext(StringValue.of("hello")); + req.onNext(StringValue.of("world")); + req.onCompleted(); + StringValue res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getValue(), is("hello world")); } @Test void testBidirectionalEcho() { - GrpcServiceDescriptor serviceDescriptor = - GrpcServiceDescriptor.builder() - .serviceName("StringService") - .putMethod("Echo", - GrpcClientMethodDescriptor.bidirectional("StringService", "Echo") - .requestType(StringValue.class) - .responseType(StringValue.class) - .build()) - .build(); - - Iterator r = grpcClient.serviceClient(serviceDescriptor) + Iterator res = grpcClient.serviceClient(serviceDescriptor) .bidi("Echo", List.of(StringValue.of("hello"), - StringValue.of("world")).iterator()); - assertThat(r.next().getValue(), is("hello")); - assertThat(r.next().getValue(), is("world")); - assertThat(r.hasNext(), is(false)); + StringValue.of("world")).iterator()); + assertThat(res.next().getValue(), is("hello")); + assertThat(res.next().getValue(), is("world")); + assertThat(res.hasNext(), is(false)); + } + + @Test + void testBidirectionalEchoAsync() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver req = grpcClient.serviceClient(serviceDescriptor) + .bidi("Echo", multiStreamObserver(future)); + req.onNext(StringValue.of("hello")); + req.onNext(StringValue.of("world")); + req.onCompleted(); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getValue(), is("hello")); + assertThat(res.next().getValue(), is("world")); + assertThat(res.hasNext(), is(false)); + } + + // -- Utility methods -- + + private static StreamObserver singleStreamObserver(CompletableFuture future) { + return new StreamObserver<>() { + private ReqT value; + + @Override + public void onNext(ReqT value) { + this.value = value; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value); + } + }; + } + + private static StreamObserver multiStreamObserver(CompletableFuture> future) { + return new StreamObserver<>() { + private final List value = new ArrayList<>(); + + @Override + public void onNext(ResT value) { + this.value.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value.iterator()); + } + }; } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java index b5f54bdd5ed..c31d3258714 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java @@ -33,19 +33,19 @@ public interface GrpcServiceClient { */ String serviceName(); - RespT unary(String methodName, ReqT request); + ResT unary(String methodName, ReqT request); - StreamObserver unary(String methodName, StreamObserver response); + void unary(String methodName, ReqT request, StreamObserver response); - Iterator serverStream(String methodName, ReqT request); + Iterator serverStream(String methodName, ReqT request); - void serverStream(String methodName, ReqT request, StreamObserver response); + void serverStream(String methodName, ReqT request, StreamObserver response); - RespT clientStream(String methodName, Iterator request); + ResT clientStream(String methodName, Iterator request); - StreamObserver clientStream(String methodName, StreamObserver response); + StreamObserver clientStream(String methodName, StreamObserver response); - Iterator bidi(String methodName, Iterator request); + Iterator bidi(String methodName, Iterator request); - StreamObserver bidi(String methodName, StreamObserver response); + StreamObserver bidi(String methodName, StreamObserver response); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index 34ac48f73bd..4f54e93b09a 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -41,35 +41,38 @@ public String serviceName() { } @Override - public RespT unary(String methodName, ReqT request) { - ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.UNARY); + public ResT unary(String methodName, ReqT request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.UNARY); return ClientCalls.blockingUnaryCall(call, request); } @Override - public StreamObserver unary(String methodName, StreamObserver response) { - return null; + public void unary(String methodName, ReqT request, StreamObserver response) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.UNARY); + ClientCalls.asyncUnaryCall(call, request, response); } @Override - public Iterator serverStream(String methodName, ReqT request) { - ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.SERVER_STREAMING); + public Iterator serverStream(String methodName, ReqT request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.SERVER_STREAMING); return ClientCalls.blockingServerStreamingCall(call, request); } @Override - public void serverStream(String methodName, ReqT request, StreamObserver response) { + public void serverStream(String methodName, ReqT request, StreamObserver response) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.SERVER_STREAMING); + ClientCalls.asyncServerStreamingCall(call, request, response); } @Override - public RespT clientStream(String methodName, Iterator request) { - ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.CLIENT_STREAMING); - CompletableFuture future = new CompletableFuture<>(); + public ResT clientStream(String methodName, Iterator request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.CLIENT_STREAMING); + CompletableFuture future = new CompletableFuture<>(); StreamObserver observer = ClientCalls.asyncClientStreamingCall(call, new StreamObserver<>() { - private RespT value; + private ResT value; @Override - public void onNext(RespT value) { + public void onNext(ResT value) { this.value = value; } @@ -99,19 +102,20 @@ public void onCompleted() { } @Override - public StreamObserver clientStream(String methodName, StreamObserver response) { - return null; + public StreamObserver clientStream(String methodName, StreamObserver response) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.CLIENT_STREAMING); + return ClientCalls.asyncClientStreamingCall(call, response); } @Override - public Iterator bidi(String methodName, Iterator request) { - ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.BIDI_STREAMING); - CompletableFuture> future = new CompletableFuture<>(); + public Iterator bidi(String methodName, Iterator request) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.BIDI_STREAMING); + CompletableFuture> future = new CompletableFuture<>(); StreamObserver observer = ClientCalls.asyncBidiStreamingCall(call, new StreamObserver<>() { - private final List values = new ArrayList<>(); + private final List values = new ArrayList<>(); @Override - public void onNext(RespT value) { + public void onNext(ResT value) { values.add(value); } @@ -141,11 +145,12 @@ public void onCompleted() { } @Override - public StreamObserver bidi(String methodName, StreamObserver response) { - return null; + public StreamObserver bidi(String methodName, StreamObserver response) { + ClientCall call = ensureMethod(methodName, MethodDescriptor.MethodType.BIDI_STREAMING); + return ClientCalls.asyncBidiStreamingCall(call, response); } - private ClientCall ensureMethod(String methodName, MethodDescriptor.MethodType methodType) { + private ClientCall ensureMethod(String methodName, MethodDescriptor.MethodType methodType) { GrpcClientMethodDescriptor method = descriptor.method(methodName); if (!method.type().equals(methodType)) { throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() From b3db2e0390ea99d5d2f30aa461ae7dcbf4993f70 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 16 Feb 2024 10:30:54 -0500 Subject: [PATCH 06/38] Fixed problem with EOS and improved logging. All basic tests are passing. --- examples/webserver/protocols/pom.xml | 8 ++ .../webclient/grpc/GrpcClientCall.java | 77 +++++++++++-------- .../webclient/grpc/GrpcClientStream.java | 23 ------ webclient/grpc/src/main/java/module-info.java | 1 + 4 files changed, 55 insertions(+), 54 deletions(-) diff --git a/examples/webserver/protocols/pom.xml b/examples/webserver/protocols/pom.xml index ff2dfc32f6e..8eeec4ab408 100644 --- a/examples/webserver/protocols/pom.xml +++ b/examples/webserver/protocols/pom.xml @@ -72,6 +72,14 @@ io.helidon.webclient helidon-webclient-grpc + + io.helidon.logging + helidon-logging-common + + + io.helidon.logging + helidon-logging-jul + org.junit.jupiter junit-jupiter-api diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index dc9a37aeaa4..55161626bb6 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -25,6 +25,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; import io.grpc.ClientCall; import io.grpc.Metadata; @@ -53,8 +54,6 @@ import io.helidon.webclient.http2.Http2StreamConfig; import io.helidon.webclient.http2.StreamTimeoutException; -import static java.lang.System.Logger.Level.DEBUG; - /** * A gRPC client call handler. The typical order of calls will be: * @@ -64,7 +63,7 @@ * @param */ class GrpcClientCall extends ClientCall { - private static final System.Logger LOGGER = System.getLogger(GrpcClientCall.class.getName()); + private static final Logger LOGGER = Logger.getLogger(GrpcClientCall.class.getName()); private static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); @@ -105,17 +104,20 @@ class GrpcClientCall extends ClientCall { @Override public void start(Listener responseListener, Metadata metadata) { + LOGGER.finest("start called"); + this.responseListener = responseListener; // obtain HTTP2 connection + ClientConnection clientConnection = clientConnection(); connection = Http2ClientConnection.create((Http2ClientImpl) grpcClient.http2Client(), - clientConnection(), true); + clientConnection, true); // create HTTP2 stream from connection clientStream = new GrpcClientStream( connection, - Http2Settings.create(), // Http2Settings - null, // SocketContext + Http2Settings.create(), // Http2Settings + clientConnection.helidonSocket(), // SocketContext new Http2StreamConfig() { @Override public boolean priorKnowledge() { @@ -129,7 +131,7 @@ public int priority() { @Override public Duration readTimeout() { - return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(60)); + return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(10)); } }, null, // Http2ClientConfig @@ -152,25 +154,27 @@ public Duration readTimeout() { @Override public void request(int numMessages) { + LOGGER.finest(() -> "request called " + numMessages); messageRequest.addAndGet(numMessages); - LOGGER.log(DEBUG, () -> "Messages requested " + numMessages); startReadBarrier.countDown(); } @Override public void cancel(String message, Throwable cause) { + LOGGER.finest(() -> "cancel called " + message); responseListener.onClose(Status.CANCELLED, new Metadata()); close(); } @Override public void halfClose() { + LOGGER.finest("halfClose called"); sendingQueue.add(EMPTY_BUFFER_DATA); // end marker } @Override public void sendMessage(ReqT message) { - // queue a message + LOGGER.finest("sendMessage called"); BufferData messageData = BufferData.growing(512); messageData.readFrom(requestMarshaller.stream(message)); BufferData headerData = BufferData.create(5); @@ -185,70 +189,80 @@ private void startStreamingThreads() { writeStreamFuture = executor.submit(() -> { try { startWriteBarrier.await(); - LOGGER.log(DEBUG, "[Writing thread] started"); + LOGGER.fine("[Writing thread] started"); + boolean endOfStream = false; while (isRemoteOpen()) { - LOGGER.log(DEBUG, "[Writing thread] polling sending queue"); + LOGGER.finest("[Writing thread] polling sending queue"); BufferData bufferData = sendingQueue.poll(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); if (bufferData != null) { if (bufferData == EMPTY_BUFFER_DATA) { // end marker - LOGGER.log(DEBUG, "[Writing thread] sending queue end marker found"); + LOGGER.finest("[Writing thread] sending queue end marker found"); + if (!endOfStream) { + LOGGER.finest("[Writing thread] sending empty buffer to end stream"); + clientStream.writeData(EMPTY_BUFFER_DATA, true); + } break; } - boolean endOfStream = (sendingQueue.peek() == EMPTY_BUFFER_DATA); - LOGGER.log(DEBUG, () -> "[Writing thread] writing bufferData " + endOfStream); + endOfStream = (sendingQueue.peek() == EMPTY_BUFFER_DATA); + boolean lastEndOfStream = endOfStream; + LOGGER.finest(() -> "[Writing thread] writing bufferData " + lastEndOfStream); clientStream.writeData(bufferData, endOfStream); } } - } catch (InterruptedException e) { - // falls through + } catch (Throwable e) { + LOGGER.finest(e.getMessage()); } - LOGGER.log(DEBUG, "[Writing thread] exiting"); + LOGGER.fine("[Writing thread] exiting"); }); // read streaming thread readStreamFuture = executor.submit(() -> { try { startReadBarrier.await(); - LOGGER.log(DEBUG, "[Reading thread] started"); + LOGGER.fine("[Reading thread] started"); // read response headers clientStream.readHeaders(); while (isRemoteOpen()) { - // attempt to send queued messages + // drain queue drainReceivingQueue(); + // trailers received? + if (clientStream.trailers().isDone()) { + LOGGER.finest("[Reading thread] trailers received"); + break; + } + // attempt to read and queue Http2FrameData frameData; try { frameData = clientStream.readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { - LOGGER.log(DEBUG, "[Reading thread] read timeout"); + LOGGER.fine("[Reading thread] read timeout"); continue; } if (frameData != null) { receivingQueue.add(frameData.data()); - LOGGER.log(DEBUG, "[Reading thread] adding bufferData to receiving queue"); - } - - // trailers received? - if (clientStream.trailers().isDone()) { - drainReceivingQueue(); // one more attempt - break; + LOGGER.finest("[Reading thread] adding bufferData to receiving queue"); } } + LOGGER.finest("[Reading thread] closing listener"); responseListener.onClose(Status.OK, new Metadata()); + } catch (Throwable e) { + LOGGER.finest(e.getMessage()); + responseListener.onClose(Status.UNKNOWN, new Metadata()); + } finally { close(); - } catch (InterruptedException e) { - // falls through } - LOGGER.log(DEBUG, "[Reading thread] exiting"); + LOGGER.fine("[Reading thread] exiting"); }); } private void close() { + LOGGER.finest("closing client call"); readStreamFuture.cancel(true); writeStreamFuture.cancel(true); sendingQueue.clear(); @@ -297,10 +311,11 @@ public int read() { } private void drainReceivingQueue() { + LOGGER.finest("[Reading thread] draining receiving queue"); while (messageRequest.get() > 0 && !receivingQueue.isEmpty()) { messageRequest.getAndDecrement(); ResT res = toResponse(receivingQueue.remove()); - LOGGER.log(DEBUG, "[Reading thread] sending response to listener"); + LOGGER.finest("[Reading thread] sending response to listener"); responseListener.onMessage(res); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java index d4a4d60e09e..0da4f3171f3 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java @@ -16,10 +16,7 @@ package io.helidon.webclient.grpc; -import io.helidon.common.buffers.BufferData; import io.helidon.common.socket.SocketContext; -import io.helidon.http.http2.Http2FrameHeader; -import io.helidon.http.http2.Http2Headers; import io.helidon.http.http2.Http2Settings; import io.helidon.webclient.http2.Http2ClientConfig; import io.helidon.webclient.http2.Http2ClientConnection; @@ -37,24 +34,4 @@ class GrpcClientStream extends Http2ClientStream { LockingStreamIdSequence streamIdSeq) { super(connection, serverSettings, ctx, http2StreamConfig, http2ClientConfig, streamIdSeq); } - - @Override - public void headers(Http2Headers headers, boolean endOfStream) { - super.headers(headers, endOfStream); - } - - @Override - public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) { - super.data(header, data, endOfStream); - } - - @Override - public void cancel() { - super.cancel(); - } - - @Override - public void close() { - super.close(); - } } diff --git a/webclient/grpc/src/main/java/module-info.java b/webclient/grpc/src/main/java/module-info.java index a13e5880898..c16b586450f 100644 --- a/webclient/grpc/src/main/java/module-info.java +++ b/webclient/grpc/src/main/java/module-info.java @@ -37,6 +37,7 @@ requires transitive io.helidon.webclient; requires io.helidon.grpc.core; + requires java.logging; exports io.helidon.webclient.grpc; From 569bd6fe169af123b287364331c4cbd0ef19e952 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 16 Feb 2024 14:59:38 -0500 Subject: [PATCH 07/38] Initial support for gRPC stubs, async only for now. --- .../src/main/resources/logging.properties | 7 +- .../webserver/protocols/GrpcStubTest.java | 250 ++++++++++++++++++ .../helidon/webclient/grpc/GrpcChannel.java | 44 +++ .../webclient/grpc/GrpcClientCall.java | 13 +- .../webclient/grpc/GrpcServiceClientImpl.java | 2 +- 5 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index d09df1098a3..63d81dadaa2 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -13,8 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # -handlers=java.util.logging.ConsoleHandler -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + # Global logging level. Can be overridden by specific loggers .level=INFO io.helidon.webserver.level=INFO +io.helidon.webclient.grpc.level=FINEST +io.helidon.webserver.grpc.level=FINEST diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java new file mode 100644 index 00000000000..77b6bcf7ed3 --- /dev/null +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java @@ -0,0 +1,250 @@ +/* + * 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.webserver.protocols; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.grpc.Channel; +import io.grpc.stub.StreamObserver; +import io.helidon.examples.grpc.strings.StringServiceGrpc; +import io.helidon.examples.grpc.strings.Strings; +import io.helidon.webclient.grpc.GrpcChannel; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class GrpcStubTest { + private static final long TIMEOUT_SECONDS = 10; + + private final GrpcClient grpcClient; + + private GrpcStubTest(GrpcClient grpcClient) { + this.grpcClient = grpcClient; + } + + @SetUpServer + public static void setup(WebServerConfig.Builder builder) { + builder.addRouting(GrpcRouting.builder() + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + GrpcStubTest::upper) + .serverStream(Strings.getDescriptor(), + "StringService", + "Split", + GrpcStubTest::split) + .clientStream(Strings.getDescriptor(), + "StringService", + "Join", + GrpcStubTest::join) + .bidi(Strings.getDescriptor(), + "StringService", + "Echo", + GrpcStubTest::echo)); + } + + // -- gRPC server methods -- + + private static Strings.StringMessage upper(Strings.StringMessage reqT) { + return Strings.StringMessage.newBuilder() + .setText(reqT.getText().toUpperCase(Locale.ROOT)) + .build(); + } + + private static void split(Strings.StringMessage reqT, + StreamObserver streamObserver) { + String[] strings = reqT.getText().split(" "); + for (String string : strings) { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(string) + .build()); + + } + streamObserver.onCompleted(); + } + + private static StreamObserver join(StreamObserver streamObserver) { + return new StreamObserver<>() { + private StringBuilder builder; + + @Override + public void onNext(Strings.StringMessage value) { + if (builder == null) { + builder = new StringBuilder(); + builder.append(value.getText()); + } else { + builder.append(" ").append(value.getText()); + } + } + + @Override + public void onError(Throwable t) { + streamObserver.onError(t); + } + + @Override + public void onCompleted() { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(builder.toString()) + .build()); + streamObserver.onCompleted(); + } + }; + } + + private static StreamObserver echo(StreamObserver streamObserver) { + return new StreamObserver<>() { + @Override + public void onNext(Strings.StringMessage value) { + streamObserver.onNext(value); + } + + @Override + public void onError(Throwable t) { + streamObserver.onError(t); + } + + @Override + public void onCompleted() { + streamObserver.onCompleted(); + } + }; + } + + // -- Tests -- + + // @Test -- blocks indefinitely + void testUnaryUpper() { + Channel channel = new GrpcChannel(grpcClient); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(channel); + Strings.StringMessage message = Strings.StringMessage.newBuilder().setText("hello").build(); + Strings.StringMessage res = service.upper(newStringMessage("hello")); + assertThat(res.getText(), is("HELLO")); + } + + @Test + void testUnaryUpperAsync() throws ExecutionException, InterruptedException, TimeoutException { + Channel channel = new GrpcChannel(grpcClient); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + CompletableFuture future = new CompletableFuture<>(); + service.upper(newStringMessage("hello"), singleStreamObserver(future)); + Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getText(), is("HELLO")); + } + + @Test + void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { + Channel channel = new GrpcChannel(grpcClient); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + CompletableFuture> future = new CompletableFuture<>(); + service.split(newStringMessage("hello world"), multiStreamObserver(future)); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); + assertThat(res.hasNext(), is(false)); + } + + @Test + void testClientStreamingJoinAsync() throws ExecutionException, InterruptedException, TimeoutException { + Channel channel = new GrpcChannel(grpcClient); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + CompletableFuture future = new CompletableFuture<>(); + StreamObserver req = service.join(singleStreamObserver(future)); + req.onNext(newStringMessage("hello")); + req.onNext(newStringMessage("world")); + req.onCompleted(); + Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getText(), is("hello world")); + } + + @Test + void testBidirectionalEchoAsync() throws ExecutionException, InterruptedException, TimeoutException { + Channel channel = new GrpcChannel(grpcClient); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver req = service.echo(multiStreamObserver(future)); + req.onNext(newStringMessage("hello")); + req.onNext(newStringMessage("world")); + req.onCompleted(); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); + assertThat(res.hasNext(), is(false)); + } + + // -- Utility methods -- + + private Strings.StringMessage newStringMessage(String data) { + return Strings.StringMessage.newBuilder().setText(data).build(); + } + + private static StreamObserver singleStreamObserver(CompletableFuture future) { + return new StreamObserver<>() { + private ReqT value; + + @Override + public void onNext(ReqT value) { + this.value = value; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value); + } + }; + } + + private static StreamObserver multiStreamObserver(CompletableFuture> future) { + return new StreamObserver<>() { + private final List value = new ArrayList<>(); + + @Override + public void onNext(ResT value) { + this.value.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value.iterator()); + } + }; + } +} 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 new file mode 100644 index 00000000000..52a88dde0d9 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java @@ -0,0 +1,44 @@ +/* + * 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.webclient.grpc; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.MethodDescriptor; +import io.helidon.webclient.api.ClientUri; + +public class GrpcChannel extends Channel { + + private final GrpcClientImpl grpcClient; + + public GrpcChannel(GrpcClient grpcClient) { + this.grpcClient = (GrpcClientImpl) grpcClient; + } + + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions callOptions) { + return new GrpcClientCall<>(grpcClient, methodDescriptor); + } + + @Override + public String authority() { + ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); + return clientUri.authority(); + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 55161626bb6..c52fe66a8e6 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -75,7 +75,7 @@ class GrpcClientCall extends ClientCall { private final ExecutorService executor; private final GrpcClientImpl grpcClient; - private final GrpcClientMethodDescriptor method; + private final MethodDescriptor methodDescriptor; private final AtomicInteger messageRequest = new AtomicInteger(); private final MethodDescriptor.Marshaller requestMarshaller; @@ -93,12 +93,11 @@ class GrpcClientCall extends ClientCall { private volatile Future readStreamFuture; private volatile Future writeStreamFuture; - @SuppressWarnings("unchecked") - GrpcClientCall(GrpcClientImpl grpcClient, GrpcClientMethodDescriptor method) { + GrpcClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor) { this.grpcClient = grpcClient; - this.method = method; - this.requestMarshaller = (MethodDescriptor.Marshaller) method.descriptor().getRequestMarshaller(); - this.responseMarshaller = (MethodDescriptor.Marshaller) method.descriptor().getResponseMarshaller(); + this.methodDescriptor = methodDescriptor; + this.requestMarshaller = methodDescriptor.getRequestMarshaller(); + this.responseMarshaller = methodDescriptor.getResponseMarshaller(); this.executor = grpcClient.webClient().executor(); } @@ -145,7 +144,7 @@ public Duration readTimeout() { WritableHeaders headers = WritableHeaders.create(); headers.add(Http2Headers.AUTHORITY_NAME, clientUri.authority()); headers.add(Http2Headers.METHOD_NAME, "POST"); - headers.add(Http2Headers.PATH_NAME, "/" + method.descriptor().getFullMethodName()); + headers.add(Http2Headers.PATH_NAME, "/" + methodDescriptor.getFullMethodName()); headers.add(Http2Headers.SCHEME_NAME, "http"); headers.add(GRPC_CONTENT_TYPE); headers.add(GRPC_ACCEPT_ENCODING); diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index 4f54e93b09a..3cb1e9a3df8 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -156,6 +156,6 @@ private ClientCall ensureMethod(String methodName, Meth throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + ", yet " + methodType + " was requested."); } - return new GrpcClientCall<>(grpcClient, method); + return new GrpcClientCall<>(grpcClient, method.descriptor()); } } From 53fdc861639910ae2c0637d47b2bb5c29c2fd1fe Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Tue, 20 Feb 2024 14:16:59 -0500 Subject: [PATCH 08/38] New test for gRPC that uses stubs. Fixes a number of checkstyle and copyright issues. --- .../webserver/protocols/ProtocolsMain.java | 2 +- .../protocols/src/main/proto/strings.proto | 1 - .../src/main/resources/logging.properties | 2 +- .../webserver/protocols/GrpcStubTest.java | 11 ++- .../webserver/protocols/GrpcTest.java | 94 ++++++++++--------- .../helidon/webclient/grpc/GrpcChannel.java | 11 ++- .../webclient/grpc/GrpcClientCall.java | 14 +-- .../grpc/GrpcClientMethodDescriptor.java | 12 ++- .../webclient/grpc/GrpcClientProtocol.java | 4 + .../webclient/grpc/GrpcProtocolProvider.java | 3 + .../webclient/grpc/GrpcServiceClient.java | 73 ++++++++++++++ .../grpc/GrpcServiceDescriptorBlueprint.java | 5 +- .../helidon/webclient/grpc/package-info.java | 20 ++++ .../helidon/webclient/http2/Http2Client.java | 2 +- .../http2/Http2ClientConnection.java | 32 ++++++- .../webclient/http2/Http2ClientImpl.java | 5 +- .../webclient/http2/Http2ClientStream.java | 48 ++++++++++ .../webclient/http2/Http2ConnectionCache.java | 15 ++- .../webclient/http2/Http2StreamConfig.java | 21 ++++- .../http2/LockingStreamIdSequence.java | 5 +- 20 files changed, 313 insertions(+), 67 deletions(-) create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/package-info.java diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java index 0a1a9fb69c2..af7e5e83bae 100644 --- a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. diff --git a/examples/webserver/protocols/src/main/proto/strings.proto b/examples/webserver/protocols/src/main/proto/strings.proto index d1a4f7be178..756b2246797 100644 --- a/examples/webserver/protocols/src/main/proto/strings.proto +++ b/examples/webserver/protocols/src/main/proto/strings.proto @@ -20,7 +20,6 @@ option java_package = "io.helidon.examples.grpc.strings"; 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) {} diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index 63d81dadaa2..74f705d03b4 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# 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. diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java index 77b6bcf7ed3..3706d4e9f1f 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java @@ -145,7 +145,6 @@ public void onCompleted() { void testUnaryUpper() { Channel channel = new GrpcChannel(grpcClient); StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(channel); - Strings.StringMessage message = Strings.StringMessage.newBuilder().setText("hello").build(); Strings.StringMessage res = service.upper(newStringMessage("hello")); assertThat(res.getText(), is("HELLO")); } @@ -160,6 +159,16 @@ void testUnaryUpperAsync() throws ExecutionException, InterruptedException, Time assertThat(res.getText(), is("HELLO")); } + @Test + void testServerStreamingSplit() { + Channel channel = new GrpcChannel(grpcClient); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(channel); + Iterator res = service.split(newStringMessage("hello world")); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); + assertThat(res.hasNext(), is(false)); + } + @Test void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { Channel channel = new GrpcChannel(grpcClient); diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java index 7e57fe5b1d8..e1ab1c99c3e 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java @@ -53,23 +53,23 @@ private GrpcTest(GrpcClient grpcClient) { .serviceName("StringService") .putMethod("Upper", GrpcClientMethodDescriptor.unary("StringService", "Upper") - .requestType(StringValue.class) - .responseType(StringValue.class) + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) .build()) .putMethod("Split", GrpcClientMethodDescriptor.serverStreaming("StringService", "Split") - .requestType(StringValue.class) - .responseType(StringValue.class) + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) .build()) .putMethod("Join", GrpcClientMethodDescriptor.clientStreaming("StringService", "Join") - .requestType(StringValue.class) - .responseType(StringValue.class) + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) .build()) .putMethod("Echo", GrpcClientMethodDescriptor.bidirectional("StringService", "Echo") - .requestType(StringValue.class) - .responseType(StringValue.class) + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) .build()) .build(); } @@ -167,91 +167,95 @@ public void onCompleted() { @Test void testUnaryUpper() { - StringValue res = grpcClient.serviceClient(serviceDescriptor) - .unary("Upper", StringValue.of("hello")); - assertThat(res.getValue(), is("HELLO")); + Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) + .unary("Upper", newStringMessage("hello")); + assertThat(res.getText(), is("HELLO")); } @Test void testUnaryUpperAsync() throws ExecutionException, InterruptedException, TimeoutException { - CompletableFuture future = new CompletableFuture<>(); + CompletableFuture future = new CompletableFuture<>(); grpcClient.serviceClient(serviceDescriptor) .unary("Upper", StringValue.of("hello"), singleStreamObserver(future)); - StringValue res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(res.getValue(), is("HELLO")); + Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getText(), is("HELLO")); } @Test void testServerStreamingSplit() { - Iterator res = grpcClient.serviceClient(serviceDescriptor) + Iterator res = grpcClient.serviceClient(serviceDescriptor) .serverStream("Split", - StringValue.of("hello world")); - assertThat(res.next().getValue(), is("hello")); - assertThat(res.next().getValue(), is("world")); + newStringMessage("hello world")); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } @Test void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { - CompletableFuture> future = new CompletableFuture<>(); + CompletableFuture> future = new CompletableFuture<>(); grpcClient.serviceClient(serviceDescriptor) .serverStream("Split", - StringValue.of("hello world"), + newStringMessage("hello world"), multiStreamObserver(future)); - Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(res.next().getValue(), is("hello")); - assertThat(res.next().getValue(), is("world")); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } @Test void testClientStreamingJoin() { - StringValue res = grpcClient.serviceClient(serviceDescriptor) - .clientStream("Join", List.of(StringValue.of("hello"), - StringValue.of("world")).iterator()); - assertThat(res.getValue(), is("hello world")); + Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) + .clientStream("Join", List.of(newStringMessage("hello"), + newStringMessage("world")).iterator()); + assertThat(res.getText(), is("hello world")); } @Test void testClientStreamingJoinAsync() throws ExecutionException, InterruptedException, TimeoutException { - CompletableFuture future = new CompletableFuture<>(); - StreamObserver req = grpcClient.serviceClient(serviceDescriptor) + CompletableFuture future = new CompletableFuture<>(); + StreamObserver req = grpcClient.serviceClient(serviceDescriptor) .clientStream("Join", singleStreamObserver(future)); - req.onNext(StringValue.of("hello")); - req.onNext(StringValue.of("world")); + req.onNext(newStringMessage("hello")); + req.onNext(newStringMessage("world")); req.onCompleted(); - StringValue res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(res.getValue(), is("hello world")); + Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.getText(), is("hello world")); } @Test void testBidirectionalEcho() { - Iterator res = grpcClient.serviceClient(serviceDescriptor) - .bidi("Echo", List.of(StringValue.of("hello"), - StringValue.of("world")).iterator()); - assertThat(res.next().getValue(), is("hello")); - assertThat(res.next().getValue(), is("world")); + Iterator res = grpcClient.serviceClient(serviceDescriptor) + .bidi("Echo", List.of(newStringMessage("hello"), + newStringMessage("world")).iterator()); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } @Test void testBidirectionalEchoAsync() throws ExecutionException, InterruptedException, TimeoutException { - CompletableFuture> future = new CompletableFuture<>(); - StreamObserver req = grpcClient.serviceClient(serviceDescriptor) + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver req = grpcClient.serviceClient(serviceDescriptor) .bidi("Echo", multiStreamObserver(future)); - req.onNext(StringValue.of("hello")); - req.onNext(StringValue.of("world")); + req.onNext(newStringMessage("hello")); + req.onNext(newStringMessage("world")); req.onCompleted(); - Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(res.next().getValue(), is("hello")); - assertThat(res.next().getValue(), is("world")); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getText(), is("hello")); + assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } // -- Utility methods -- + private Strings.StringMessage newStringMessage(String data) { + return Strings.StringMessage.newBuilder().setText(data).build(); + } + private static StreamObserver singleStreamObserver(CompletableFuture future) { return new StreamObserver<>() { private ReqT value; 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 52a88dde0d9..f885af221f7 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 @@ -16,16 +16,25 @@ package io.helidon.webclient.grpc; +import io.helidon.webclient.api.ClientUri; + import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.MethodDescriptor; -import io.helidon.webclient.api.ClientUri; +/** + * Helidon's implementation of a gRPC {@link Channel}. + */ public class GrpcChannel extends Channel { private final GrpcClientImpl grpcClient; + /** + * Creates a new channel from a {@link GrpcClient}. + * + * @param grpcClient the gRPC client + */ public GrpcChannel(GrpcClient grpcClient) { this.grpcClient = (GrpcClientImpl) grpcClient; } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index c52fe66a8e6..029c713c0b4 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -27,10 +27,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; -import io.grpc.ClientCall; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.Status; import io.helidon.common.buffers.BufferData; import io.helidon.common.tls.Tls; import io.helidon.http.Header; @@ -54,6 +50,11 @@ import io.helidon.webclient.http2.Http2StreamConfig; import io.helidon.webclient.http2.StreamTimeoutException; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; + /** * A gRPC client call handler. The typical order of calls will be: * @@ -162,6 +163,8 @@ public void request(int numMessages) { public void cancel(String message, Throwable cause) { LOGGER.finest(() -> "cancel called " + message); responseListener.onClose(Status.CANCELLED, new Metadata()); + readStreamFuture.cancel(true); + writeStreamFuture.cancel(true); close(); } @@ -262,11 +265,10 @@ private void startStreamingThreads() { private void close() { LOGGER.finest("closing client call"); - readStreamFuture.cancel(true); - writeStreamFuture.cancel(true); sendingQueue.clear(); clientStream.cancel(); connection.close(); + LOGGER.finest("closing client call ends"); } private ClientConnection clientConnection() { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index 81a1630dbda..3c35e380477 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -20,14 +20,15 @@ import java.util.Collections; import java.util.List; -import io.grpc.CallCredentials; -import io.grpc.ClientInterceptor; -import io.grpc.MethodDescriptor; import io.helidon.grpc.core.InterceptorPriorities; import io.helidon.grpc.core.MarshallerSupplier; import io.helidon.grpc.core.MethodHandler; import io.helidon.grpc.core.PriorityBag; +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 io.grpc.MethodDescriptor}, this class also holds the request and response @@ -203,6 +204,11 @@ public MethodDescriptor descriptor() { return (MethodDescriptor) descriptor; } + /** + * Returns the {@link MethodDescriptor.MethodType} of this method. + * + * @return the method type + */ public MethodDescriptor.MethodType type() { return descriptor.getType(); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java index 84e96b1191d..b701edd8f9c 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java @@ -23,6 +23,10 @@ import io.helidon.webclient.http2.Http2ClientConfig; class GrpcClientProtocol { + + private GrpcClientProtocol() { + } + static GrpcClientStream create(SocketContext scoketContext, Http2Settings serverSettings, Http2ClientConfig clientConfig, diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java index 3c63f56ae8e..7ea3122e521 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java @@ -19,6 +19,9 @@ import io.helidon.webclient.api.WebClient; import io.helidon.webclient.spi.ClientProtocolProvider; +/** + * Provider for {@link GrpcClient}. + */ public class GrpcProtocolProvider implements ClientProtocolProvider { static final String CONFIG_KEY = "grpc"; diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java index c31d3258714..4af0bede8f6 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java @@ -26,6 +26,7 @@ * @see io.helidon.webclient.grpc.GrpcClient#serviceClient(io.helidon.webclient.grpc.GrpcServiceDescriptor) */ public interface GrpcServiceClient { + /** * Name of the service this client was created for. * @@ -33,19 +34,91 @@ public interface GrpcServiceClient { */ String serviceName(); + /** + * Blocking gRPC unary call. + * + * @param methodName method name + * @param request the request + * @return the response + * @param type of request + * @param type of response + */ ResT unary(String methodName, ReqT request); + /** + * Asynchronous gRPC unary call. + * + * @param methodName method name + * @param request the request + * @param response the response observer + * @param type of request + * @param type of response + */ void unary(String methodName, ReqT request, StreamObserver response); + /** + * Blocking gRPC server stream call. + * + * @param methodName method name + * @param request the request + * @return the response iterator + * @param type of request + * @param type of response + */ Iterator serverStream(String methodName, ReqT request); + /** + * Asynchronous gRPC server stream call. + * + * @param methodName method name + * @param request the request + * @param response the response observer + * @param type of request + * @param type of response + */ void serverStream(String methodName, ReqT request, StreamObserver response); + /** + * Blocking gRPC client stream call. + * + * @param methodName method name + * @param request the request iterator + * @return the response + * @param type of request + * @param type of response + */ ResT clientStream(String methodName, Iterator request); + /** + * Asynchronous gRPC client stream call. + * + * @param methodName method name + * @param response the response observer + * @return the request observer + * @param type of request + * @param type of response + */ StreamObserver clientStream(String methodName, StreamObserver response); + /** + * gRPC bidirectional call using {@link Iterator}. + * + * @param methodName method name + * @param request request iterator + * @return response iterator + * @param type of request + * @param type of response + */ Iterator bidi(String methodName, Iterator request); + /** + * gRPC bidirectional call using {@link StreamObserver}. + * + * @param methodName method name + * @param response the response observer + * @return the request observer + * @param type of request + * @param type of response + */ StreamObserver bidi(String methodName, StreamObserver response); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java index 00fff468699..0693528e6fc 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java @@ -20,11 +20,12 @@ import java.util.NoSuchElementException; import java.util.Optional; -import io.grpc.CallCredentials; -import io.grpc.ClientInterceptor; import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; +import io.grpc.CallCredentials; +import io.grpc.ClientInterceptor; + @Prototype.Blueprint interface GrpcServiceDescriptorBlueprint { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/package-info.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/package-info.java new file mode 100644 index 00000000000..f7918d9e325 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/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. + */ + +/** + * Helidon WebClient HTTP/1.1 Support. + */ +package io.helidon.webclient.grpc; diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java index 286731ba1d9..72fc534aab1 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2Client.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java index 32cb9b903f9..fa92a9e7561 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -61,6 +61,9 @@ import static java.lang.System.Logger.Level.TRACE; import static java.lang.System.Logger.Level.WARNING; +/** + * Represents an HTTP2 connection on the client. + */ public class Http2ClientConnection { private static final System.Logger LOGGER = System.getLogger(Http2ClientConnection.class.getName()); private static final int FRAME_HEADER_LENGTH = 9; @@ -103,6 +106,14 @@ public class Http2ClientConnection { this.writer = new Http2ConnectionWriter(connection.helidonSocket(), connection.writer(), List.of()); } + /** + * Creates an HTTP2 client connection. + * + * @param http2Client the HTTP2 client + * @param connection the client connection + * @param sendSettings whether to send the settings or not + * @return an HTTP2 client connection + */ public static Http2ClientConnection create(Http2ClientImpl http2Client, ClientConnection connection, boolean sendSettings) { @@ -136,6 +147,11 @@ Http2ClientStream stream(int streamId) { } + /** + * Stream ID sequence. + * + * @return the ID sequence + */ public LockingStreamIdSequence streamIdSequence() { return streamIdSeq; } @@ -151,6 +167,12 @@ Http2ClientStream createStream(Http2StreamConfig config) { return stream; } + /** + * Adds a stream to the connection. + * + * @param streamId the stream ID + * @param stream the stream + */ public void addStream(int streamId, Http2ClientStream stream) { Lock lock = streamsLock.writeLock(); lock.lock(); @@ -161,6 +183,11 @@ public void addStream(int streamId, Http2ClientStream stream) { } } + /** + * Removes a stream from the connection. + * + * @param streamId the stream ID + */ public void removeStream(int streamId) { Lock lock = streamsLock.writeLock(); lock.lock(); @@ -206,6 +233,9 @@ void updateLastStreamId(int lastStreamId) { this.lastStreamId = lastStreamId; } + /** + * Closes this connection. + */ public void close() { this.goAway(0, Http2ErrorCode.NO_ERROR, "Closing connection"); if (state.getAndSet(State.CLOSED) != State.CLOSED) { diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java index c45c66f44e1..2381459b45e 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -27,6 +27,9 @@ import io.helidon.webclient.api.WebClient; import io.helidon.webclient.spi.HttpClientSpi; +/** + * Implementation of HTTP2 client. + */ public class Http2ClientImpl implements Http2Client, HttpClientSpi { private final WebClient webClient; private final Http2ClientConfig clientConfig; diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index 351d6558ce0..5321b846d81 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -53,6 +53,10 @@ import static java.lang.System.Logger.Level.DEBUG; +/** + * Represents an HTTP2 client stream. This class is not intended to be used by + * applications, it is only public internally within Helidon. + */ public class Http2ClientStream implements Http2Stream, ReleasableResource { private static final System.Logger LOGGER = System.getLogger(Http2ClientStream.class.getName()); @@ -182,6 +186,11 @@ void trailers(Http2Headers headers, boolean endOfStream) { trailers.complete(headers.httpHeaders()); } + /** + * Future that shall be completed once trailers are received. + * + * @return the completable future + */ public CompletableFuture trailers() { return trailers; } @@ -190,6 +199,9 @@ boolean hasEntity() { return hasEntity; } + /** + * Cancels this client stream. + */ public void cancel() { if (NON_CANCELABLE.contains(state)) { return; @@ -206,6 +218,9 @@ public void cancel() { } } + /** + * Removes the stream from underlying connection. + */ public void close() { connection.removeStream(streamId); } @@ -227,6 +242,11 @@ BufferData read(int i) { return read(); } + /** + * Reads a buffer data from the stream. + * + * @return the buffer data + */ public BufferData read() { while (state == Http2StreamState.HALF_CLOSED_LOCAL && readState != ReadState.END && hasEntity) { Http2FrameData frameData = readOne(timeout); @@ -258,6 +278,12 @@ Status waitFor100Continue() { return null; } + /** + * Writes HTTP2 headers to the stream. + * + * @param http2Headers the headers + * @param endOfStream end of stream marker + */ public void writeHeaders(Http2Headers http2Headers, boolean endOfStream) { this.state = Http2StreamState.checkAndGetState(this.state, Http2FrameType.HEADERS, true, endOfStream, true); this.readState = readState.check(http2Headers.httpHeaders().contains(HeaderValues.EXPECT_100) @@ -294,6 +320,12 @@ public void writeHeaders(Http2Headers http2Headers, boolean endOfStream) { } } + /** + * Writes a buffer data into the stream. + * + * @param entityBytes buffer data + * @param endOfStream end of stream marker + */ public void writeData(BufferData entityBytes, boolean endOfStream) { Http2FrameHeader frameHeader = Http2FrameHeader.create(entityBytes.available(), Http2FrameTypes.DATA, @@ -305,6 +337,11 @@ public void writeData(BufferData entityBytes, boolean endOfStream) { splitAndWrite(frameData); } + /** + * Reads headers from this stream. + * + * @return the headers + */ public Http2Headers readHeaders() { while (readState == ReadState.HEADERS) { Http2FrameData frameData = readOne(timeout); @@ -315,10 +352,21 @@ public Http2Headers readHeaders() { return currentHeaders; } + /** + * Returns the socket context associated with the stream. + * + * @return the socket context + */ public SocketContext ctx() { return ctx; } + /** + * Reads an HTTP2 frame from the stream. + * + * @param pollTimeout timeout + * @return the data frame + */ public Http2FrameData readOne(Duration pollTimeout) { Http2FrameData frameData = buffer.poll(pollTimeout); diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java index f50d1e36758..623e5315bb5 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ConnectionCache.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -29,6 +29,9 @@ import io.helidon.webclient.http1.Http1ClientResponse; import io.helidon.webclient.spi.ClientConnectionCache; +/** + * A cache of HTTP2 connections. + */ public final class Http2ConnectionCache extends ClientConnectionCache { private static final Http2ConnectionCache SHARED = new Http2ConnectionCache(true); private final LruCache http2Supported = LruCache.builder() @@ -41,10 +44,20 @@ private Http2ConnectionCache(boolean shared) { super(shared); } + /** + * Returns a reference to the shared connection cache. + * + * @return shared connection cache + */ public static Http2ConnectionCache shared() { return SHARED; } + /** + * Creates a fresh connection cache. + * + * @return new connection cache + */ public static Http2ConnectionCache create() { return new Http2ConnectionCache(false); } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java index 29bc30b5032..61cffcf6b9d 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2StreamConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -18,10 +18,29 @@ import java.time.Duration; +/** + * Configuration for an HTTP2 stream. + */ public interface Http2StreamConfig { + + /** + * Prior knowledge setting. + * + * @return prior knowledge setting + */ boolean priorKnowledge(); + /** + * Stream priority. + * + * @return the stream priority + */ int priority(); + /** + * Read timeout for this stream. + * + * @return the timeout + */ Duration readTimeout(); } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java index 6df24cc34c9..cb6b53c0084 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/LockingStreamIdSequence.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -19,6 +19,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +/** + * A stream ID sequence that uses locks for concurrency. + */ public class LockingStreamIdSequence { private final AtomicInteger streamIdSeq = new AtomicInteger(0); From 0f334861d2f79934a9d7688fbc8c5946147d5605 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 21 Feb 2024 10:32:28 -0500 Subject: [PATCH 09/38] Use WebClient to create GrpcClient(s). --- .../webserver/protocols/GrpcStubTest.java | 33 +++++++++---------- .../io/helidon/webclient/grpc/GrpcClient.java | 10 ++++++ .../webclient/grpc/GrpcClientImpl.java | 6 ++++ .../webclient/grpc/GrpcProtocolProvider.java | 5 +-- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java index 3706d4e9f1f..2f6a14c1f82 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java @@ -25,11 +25,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import io.grpc.Channel; import io.grpc.stub.StreamObserver; import io.helidon.examples.grpc.strings.StringServiceGrpc; import io.helidon.examples.grpc.strings.Strings; -import io.helidon.webclient.grpc.GrpcChannel; +import io.helidon.webclient.api.WebClient; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.grpc.GrpcRouting; @@ -44,10 +43,10 @@ class GrpcStubTest { private static final long TIMEOUT_SECONDS = 10; - private final GrpcClient grpcClient; + private final WebClient webClient; - private GrpcStubTest(GrpcClient grpcClient) { - this.grpcClient = grpcClient; + private GrpcStubTest(WebClient webClient) { + this.webClient = webClient; } @SetUpServer @@ -143,16 +142,16 @@ public void onCompleted() { // @Test -- blocks indefinitely void testUnaryUpper() { - Channel channel = new GrpcChannel(grpcClient); - StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(channel); + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); Strings.StringMessage res = service.upper(newStringMessage("hello")); assertThat(res.getText(), is("HELLO")); } @Test void testUnaryUpperAsync() throws ExecutionException, InterruptedException, TimeoutException { - Channel channel = new GrpcChannel(grpcClient); - StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient.channel()); CompletableFuture future = new CompletableFuture<>(); service.upper(newStringMessage("hello"), singleStreamObserver(future)); Strings.StringMessage res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); @@ -161,8 +160,8 @@ void testUnaryUpperAsync() throws ExecutionException, InterruptedException, Time @Test void testServerStreamingSplit() { - Channel channel = new GrpcChannel(grpcClient); - StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(channel); + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); Iterator res = service.split(newStringMessage("hello world")); assertThat(res.next().getText(), is("hello")); assertThat(res.next().getText(), is("world")); @@ -171,8 +170,8 @@ void testServerStreamingSplit() { @Test void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { - Channel channel = new GrpcChannel(grpcClient); - StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient.channel()); CompletableFuture> future = new CompletableFuture<>(); service.split(newStringMessage("hello world"), multiStreamObserver(future)); Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); @@ -183,8 +182,8 @@ void testServerStreamingSplitAsync() throws ExecutionException, InterruptedExcep @Test void testClientStreamingJoinAsync() throws ExecutionException, InterruptedException, TimeoutException { - Channel channel = new GrpcChannel(grpcClient); - StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient.channel()); CompletableFuture future = new CompletableFuture<>(); StreamObserver req = service.join(singleStreamObserver(future)); req.onNext(newStringMessage("hello")); @@ -196,8 +195,8 @@ void testClientStreamingJoinAsync() throws ExecutionException, InterruptedExcept @Test void testBidirectionalEchoAsync() throws ExecutionException, InterruptedException, TimeoutException { - Channel channel = new GrpcChannel(grpcClient); - StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(channel); + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient.channel()); CompletableFuture> future = new CompletableFuture<>(); StreamObserver req = service.echo(multiStreamObserver(future)); req.onNext(newStringMessage("hello")); 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 9eb07db371c..d86950b215d 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 @@ -18,6 +18,7 @@ import java.util.function.Consumer; +import io.grpc.Channel; import io.helidon.builder.api.RuntimeType; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.spi.Protocol; @@ -27,6 +28,8 @@ */ @RuntimeType.PrototypedBy(GrpcClientConfig.class) public interface GrpcClient extends RuntimeType.Api { + String PROTOCOL_ID = "grpc"; + /** * Protocol to use to obtain an instance of gRPC specific client from * {@link io.helidon.webclient.api.WebClient#client(io.helidon.webclient.spi.Protocol)}. @@ -80,4 +83,11 @@ static GrpcClient create() { * @return client for the provided descriptor */ GrpcServiceClient serviceClient(GrpcServiceDescriptor descriptor); + + /** + * Create a gRPC channel for this client that can be used to create stubs. + * + * @return a new gRPC channel + */ + Channel channel(); } 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 bf0934074d6..903dcfb2657 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 @@ -16,6 +16,7 @@ package io.helidon.webclient.grpc; +import io.grpc.Channel; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.http2.Http2Client; @@ -47,4 +48,9 @@ public GrpcClientConfig prototype() { public GrpcServiceClient serviceClient(GrpcServiceDescriptor descriptor) { return new GrpcServiceClientImpl(descriptor, this); } + + @Override + public Channel channel() { + return new GrpcChannel(this); + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java index 7ea3122e521..43b10f17f40 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java @@ -33,7 +33,7 @@ public GrpcProtocolProvider() { @Override public String protocolId() { - return CONFIG_KEY; + return GrpcClient.PROTOCOL_ID; } @Override @@ -49,7 +49,8 @@ public GrpcClientProtocolConfig defaultConfig() { @Override public GrpcClient protocol(WebClient client, GrpcClientProtocolConfig config) { return new GrpcClientImpl(client, - GrpcClientConfig.builder().from(client.prototype()) + GrpcClientConfig.builder() + .from(client.prototype()) .protocolConfig(config) .buildPrototype()); } From bc2419d36e611306c5197a02fa347b9bf79ec46f Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 21 Feb 2024 14:26:07 -0500 Subject: [PATCH 10/38] Fixed unary blocking test. --- .../webserver/protocols/GrpcStubTest.java | 2 +- .../helidon/webclient/grpc/GrpcChannel.java | 4 +-- .../webclient/grpc/GrpcClientCall.java | 25 +++++++++++++++++-- .../webclient/grpc/GrpcServiceClientImpl.java | 3 ++- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java index 2f6a14c1f82..15eceb35b4c 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java @@ -140,7 +140,7 @@ public void onCompleted() { // -- Tests -- - // @Test -- blocks indefinitely + @Test void testUnaryUpper() { GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); 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 f885af221f7..80767f1cce7 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}. */ -public class GrpcChannel extends Channel { +class GrpcChannel extends Channel { private final GrpcClientImpl grpcClient; @@ -42,7 +42,7 @@ public GrpcChannel(GrpcClient grpcClient) { @Override public ClientCall newCall( MethodDescriptor methodDescriptor, CallOptions callOptions) { - return new GrpcClientCall<>(grpcClient, methodDescriptor); + return new GrpcClientCall<>(grpcClient, methodDescriptor, callOptions); } @Override diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 029c713c0b4..e4d71f97710 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.Collections; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; @@ -50,6 +51,7 @@ import io.helidon.webclient.http2.Http2StreamConfig; import io.helidon.webclient.http2.StreamTimeoutException; +import io.grpc.CallOptions; import io.grpc.ClientCall; import io.grpc.Metadata; import io.grpc.MethodDescriptor; @@ -77,6 +79,7 @@ class GrpcClientCall extends ClientCall { private final ExecutorService executor; private final GrpcClientImpl grpcClient; private final MethodDescriptor methodDescriptor; + private final CallOptions callOptions; private final AtomicInteger messageRequest = new AtomicInteger(); private final MethodDescriptor.Marshaller requestMarshaller; @@ -94,9 +97,10 @@ class GrpcClientCall extends ClientCall { private volatile Future readStreamFuture; private volatile Future writeStreamFuture; - GrpcClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor) { + GrpcClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { this.grpcClient = grpcClient; this.methodDescriptor = methodDescriptor; + this.callOptions = callOptions; this.requestMarshaller = methodDescriptor.getRequestMarshaller(); this.responseMarshaller = methodDescriptor.getResponseMarshaller(); this.executor = grpcClient.webClient().executor(); @@ -268,7 +272,24 @@ private void close() { sendingQueue.clear(); clientStream.cancel(); connection.close(); - LOGGER.finest("closing client call ends"); + unblockUnaryExecutor(); + } + + /** + * Unary blocking calls that use stubs provide their own executor which needs + * to be used at least once to unblock the calling thread and complete the + * gRPC invocation. This method submits an empty task for that purpose. There + * may be a better way to achieve this. + */ + private void unblockUnaryExecutor() { + Executor executor = callOptions.getExecutor(); + if (executor != null) { + try { + executor.execute(() -> {}); + } catch (Throwable t) { + // ignored + } + } } private ClientConnection clientConnection() { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index 3cb1e9a3df8..c51a46e4a9d 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import io.grpc.CallOptions; import io.grpc.ClientCall; import io.grpc.MethodDescriptor; import io.grpc.stub.ClientCalls; @@ -156,6 +157,6 @@ private ClientCall ensureMethod(String methodName, Meth throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + ", yet " + methodType + " was requested."); } - return new GrpcClientCall<>(grpcClient, method.descriptor()); + return new GrpcClientCall<>(grpcClient, method.descriptor(), CallOptions.DEFAULT); } } From cf16c2284c33e34ff9f7aa323f69ca9349ff8507 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Thu, 22 Feb 2024 15:26:22 -0500 Subject: [PATCH 11/38] Support for TLS. Switched tests to use TLS. --- .../webserver/protocols/GrpcStubTest.java | 63 ++++++++++++----- .../webserver/protocols/GrpcTest.java | 66 ++++++++++++------ .../protocols/src/test/resources/client.p12 | Bin 0 -> 4181 bytes .../protocols/src/test/resources/server.p12 | Bin 0 -> 4133 bytes .../webclient/grpc/GrpcClientCall.java | 16 ++--- 5 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 examples/webserver/protocols/src/test/resources/client.p12 create mode 100644 examples/webserver/protocols/src/test/resources/server.p12 diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java index 15eceb35b4c..43640d20bcc 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java @@ -26,10 +26,13 @@ import java.util.concurrent.TimeoutException; import io.grpc.stub.StreamObserver; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; import io.helidon.examples.grpc.strings.StringServiceGrpc; import io.helidon.examples.grpc.strings.Strings; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.WebServer; import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.ServerTest; @@ -39,35 +42,57 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +/** + * Tests gRPC client using stubs and TLS. + */ @ServerTest class GrpcStubTest { private static final long TIMEOUT_SECONDS = 10; private final WebClient webClient; - private GrpcStubTest(WebClient webClient) { - this.webClient = webClient; + private GrpcStubTest(WebServer server) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.webClient = WebClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + server.port()) + .build(); } @SetUpServer public static void setup(WebServerConfig.Builder builder) { - builder.addRouting(GrpcRouting.builder() - .unary(Strings.getDescriptor(), - "StringService", - "Upper", - GrpcStubTest::upper) - .serverStream(Strings.getDescriptor(), - "StringService", - "Split", - GrpcStubTest::split) - .clientStream(Strings.getDescriptor(), - "StringService", - "Join", - GrpcStubTest::join) - .bidi(Strings.getDescriptor(), - "StringService", - "Echo", - GrpcStubTest::echo)); + builder.tls(tls -> tls.privateKey(key -> key + .keystore(store -> store + .passphrase("password") + .keystore(Resource.create("server.p12")))) + .privateKeyCertChain(key -> key + .keystore(store -> store + .trustStore(true) + .passphrase("password") + .keystore(Resource.create("server.p12"))))) + .addRouting(GrpcRouting.builder() + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + GrpcStubTest::upper) + .serverStream(Strings.getDescriptor(), + "StringService", + "Split", + GrpcStubTest::split) + .clientStream(Strings.getDescriptor(), + "StringService", + "Join", + GrpcStubTest::join) + .bidi(Strings.getDescriptor(), + "StringService", + "Echo", + GrpcStubTest::echo)); } // -- gRPC server methods -- diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java index e1ab1c99c3e..a8c3c0a093e 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java +++ b/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java @@ -27,10 +27,13 @@ import com.google.protobuf.StringValue; import io.grpc.stub.StreamObserver; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; import io.helidon.examples.grpc.strings.Strings; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import io.helidon.webserver.WebServer; import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.ServerTest; @@ -40,6 +43,9 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +/** + * Tests gRPC client using low-level API and TLS, no stubs. + */ @ServerTest class GrpcTest { private static final long TIMEOUT_SECONDS = 10; @@ -47,8 +53,19 @@ class GrpcTest { private final GrpcClient grpcClient; private final GrpcServiceDescriptor serviceDescriptor; - private GrpcTest(GrpcClient grpcClient) { - this.grpcClient = grpcClient; + private GrpcTest(WebServer server) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + server.port()) + .build(); + this.serviceDescriptor = GrpcServiceDescriptor.builder() .serviceName("StringService") .putMethod("Upper", @@ -76,23 +93,32 @@ private GrpcTest(GrpcClient grpcClient) { @SetUpServer public static void setup(WebServerConfig.Builder builder) { - builder.addRouting(GrpcRouting.builder() - .unary(Strings.getDescriptor(), - "StringService", - "Upper", - GrpcTest::upper) - .serverStream(Strings.getDescriptor(), - "StringService", - "Split", - GrpcTest::split) - .clientStream(Strings.getDescriptor(), - "StringService", - "Join", - GrpcTest::join) - .bidi(Strings.getDescriptor(), - "StringService", - "Echo", - GrpcTest::echo)); + builder.tls(tls -> tls.privateKey(key -> key + .keystore(store -> store + .passphrase("password") + .keystore(Resource.create("server.p12")))) + .privateKeyCertChain(key -> key + .keystore(store -> store + .trustStore(true) + .passphrase("password") + .keystore(Resource.create("server.p12"))))) + .addRouting(GrpcRouting.builder() + .unary(Strings.getDescriptor(), + "StringService", + "Upper", + GrpcTest::upper) + .serverStream(Strings.getDescriptor(), + "StringService", + "Split", + GrpcTest::split) + .clientStream(Strings.getDescriptor(), + "StringService", + "Join", + GrpcTest::join) + .bidi(Strings.getDescriptor(), + "StringService", + "Echo", + GrpcTest::echo)); } // -- gRPC server methods -- @@ -187,7 +213,7 @@ void testUnaryUpperAsync() throws ExecutionException, InterruptedException, Time void testServerStreamingSplit() { Iterator res = grpcClient.serviceClient(serviceDescriptor) .serverStream("Split", - newStringMessage("hello world")); + newStringMessage("hello world")); assertThat(res.next().getText(), is("hello")); assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); diff --git a/examples/webserver/protocols/src/test/resources/client.p12 b/examples/webserver/protocols/src/test/resources/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..4eb3b8325cd0190163032aca86e1d4fc94856822 GIT binary patch literal 4181 zcmY+EWmFW5wuWIGKw^;Y8d`FO4r!R7kra{cRN$kVp>ybzA%_qK=@29&1VMz6MnqaE zk#3Inu6xf}=f_@qt>@kQ{rNx;WcmQy2M`1q5k4Vbta|J@2$ul21o7w^hgIE5dsUMBr)v#p-`Y6`%P3zDS600VN3FH3R`Xhdjn7`hWbloDoRH)IP6+WcmE9 z`35_>AJFE&UKlM8cz`qTw+lF;|4Y44{>C>)$b58Pwv%K0R(%IXj}~QHjD4NBVYZjc zF@t^Op?!4hS2W1WsUs*fH4rKzE`)r~ui`YVNmNGREmp`fmhZ-l6^5sfB`zPY40}#g zH)^o?@o9hBqDnY3fTdikVzhQYjJ@QFySfUF2zyQ%MJkSx&6yp$d%0u5zcM8tP1i^6 z%}M{O^-J(q-l}? zG;sYG<`hB67OAJ{Kn7v!iM%17ZGOW*_+6?mj_PPjWm!Ul8pMIbf!M^ zWgBVP1D^1uoR+3$()AEJwCJQVGFXYpP?{E!vJd6qcaPa-_wk2fi>CtS#t_Q=YzDhg z_T{E?`*O_~!8+lV;#JIN%WYb`Smp6z>%jU|E0Q|UwSakO!YAE{b)<{PF*8og zVze-4xbFhS3hi4{gv+TZ5{{zgoi6OkC#C!n|C=?7gif&4$VS7{f>r$*glAp@OguSO zga@4c?5V%spuxJq+7~S4uO0~Qtx@pLpVA#omKA$K=0qs_or+yj(CH>JtF!DYfsUdj#Y;j*a+4940>!Es- zM}7u;^}?V7TD&gvN+Kp6EAKaV;pn^2wV@?@vl^3I+gH5IuR|iG=L7^PT$x(L9Pe@2 z?=%-f?!NUux8M^q@js$G+ipgHOQVf`dy{^6RCxNjjhLfHs9_L9h593q(M55&T#~hF z@ZHR#^3^uOTItFm*F)8$)0x`}Zp|%D6NEgD^>WL7HNjNpku0LT6DS&tU+qTrHbUu9 z&)^rxWz@YiTBES@?NpTL!5omgXlbo6pb6g`>>=vt1vu!E<3vBcdqHR_Ba4|Eb_xGg z)pJ##He7jY95Ya1>hZEv-!3&R>CaoXf5J1TZ)6O9K~lf--8>#_9ORnq@zOo*O|s_- zH>^L{5fkgqYi9TBfU#YG%Akqk_WHE*j6&Si&2D2c`4YK3)g}>K$KvCQ7`x9(WDsEy zuQH5qIeN|NDn3hDL$9r!IAXNK`>ba0F{990Ft$^8<0CG8rrA~9aBG;US|w!VjlFOnx;asOlp6HSq&uB> zPp2hwGj;nLOA8~DC4JrPmu=91AuurQp^IZMg8;( zNk?BgR!9GmWnhg;q@`&luP<%zDo;OcX`dMoFJwV0VQyK&8YX2%_YF9}(b}Ac#W$qCfyH z&fn$zp9J^6yo&!nUaeG&iy}_w@w&JB$t=SwN~qqp*!;g?+nl!qbt-3Srz zdUU=~v7Sgb0VsS}2&|z~|3MrAAtr}=3)n1hxrwEjX74xY&WumZJgpiDImLH>0HGEj zE@Pz6+gd485YSPSd-~MF_ofwqC9vZ}8J_A_xnnt@2jF{VD_){puWF0@1B$Q~N$0Vs z-?9BDZRR}ZLhMq|y5-J;bH5!%c&I1&i~bmo_W>~SP^HW#(~yyFh0IX*un{<(Z*H5* zG3|bt;nH9dulFr4zGP97g%w$luP$lQCw&7zUrnj|ct{dHQaAqrM}sXWo* zIwbhHK4sYTi}c}+H=ECU_CW)XPNsksw*O(5%WXUzxJ;CW4wY-#fhx7#RxUv{vGW)O zD08=SPkJ@3D3mcw@tOyy;4L~ar`xu7adVYdzfGQS4)LplDy z?4mahFGu`fqOEB?-}?;U;~_d$fZX81i-)m3$WLiGrYR{Vb(+BxFB{bgmoIBI=R^T$ z2jb^~oKDwEmh%hg@%Nu7LXSBz15KET`V3br4R4cQx~+zqc5kP!I37*ccaqajWobMK zZi-eDvt^V0Wx5yXv-L$NFi$%&6yL^6h{)OAOJ(=Ff06E!^>9J;{ZS0mp#M!8;!a)0 ziI!n_qQZSi)bXC}Av&X{1=>a_R`JQ(6a=y?=Xs3hk0yi^%Qtn{d`)D4<(!F3-FsJ5 zfY|6y$E7qMM(%l|mdCYd7K?3kA#e(=E7g{U4qi2#Z$_ndvFhE0Oh3#!n`A1vNw<3W z2uInJ(>ciz+w6ik()9M}hRO6sepzQ5EC$ppZo^+t8|M>ZX4rADe+_-TVMhM%sw z9eW4be3ydc95|A1=ww9x$mFh{%P|`7Y<{Ga>VGJRJ%>eS&vgD{K%AZsPcYh zR}~?4Sn|7?H;%kfyI~CMyewJu8rWrh+pR0NYq-Iyq&?|pGN4B-TYzWrbyp>!A<)38 z4%1Lg&0jg5u$U@}6z(^3tDDy`9}Qvpeg^x)O5ZI8it`!f*`5Kvgjs4Q##u&}*=DiI zn!gK5ddr7~KTeHsx+V>5JIb!U`qpgU)F>16$TyDG#KhQ%jGy|+YU~O=2kCWLwf7N0 zX%Sbo>AZn%B0h?8<4)6dI~>r`P6LmtinE+;OU)O|Wex9p(ws2hCJo+9_pFiB z7fAm7$^ThSDziqaaV4p!zZcH?1IeFP&zt$b9G{haDnBg97{BIqH&x|~TRr{mNtgF1 z=KD*IKX7d`AHJ*>xJ?0B3;EYPe-a3nrJ&`HtY_0RBG!7dK>bgh3!%qzVTg$*y+MY) z&$Z8|T}^|k`2EED{c_d(304<g-Im@iW9sI{w z4KPkbki~?=Wtp>|bG;b~`LW$2ht1I_SHKB9W3!PrEptc|R1Yy; zL@g}P*5dF&^1AI!S0rw>;(g?>MWY)#lX^iM*AL>3H>kVuE63I@@!OM<{gtNgl29Y= z$^`X5{qhOWJ2HWP{AE~d%@UaqJ_L+dy2JO@AXYtb^96==Ut?GO`W&^~Le97!;K3a` z=?l=}772&XuzBVWt*y=8MfJjUx6iG4BNV{th8^B9N^x2wm{R^G=X52#O9B1MWoAC@ zp1xqHET!2Hy`ewqcZvU%1Frz-;E~+EAuqN@lnWGN)N|X2^itS8+MkaTv9oe9ldamr zpS2{JVzL@2Bvi}=Nii~+uY+@+OR18n)q#s5o=uczm)Acyy)PdGb)79|j7*YoAE07> zR`N%M{!UWAahsr!##wZg{a)X~h#T|vN!gy2`{vaiqcq`c)c|6%UCUNfJ1>%-T5l-b zp=s4{-N=IPAX0nM)8>w{-~|ginZhrAq)NOaoJC7sHpFg9jZ>KDYsR~;OtGzR=ai>k zg2qili0Iyw1YyE!f{P65K)rcr%JX_h-u#w=ZyG}lmZ{`U9e*Yg_e>8T-4(kX%B^`0 z9PO7HKCu0@&@-2{$|71S(wKBumamE%t#&9d$rL>_dPFn^H(cc8$D0dwetT0u~E zav=YHx$|t?RjAix^BY5*cG-M6dW?aGScPcvZ1WaFRz*emZ-6;-XsdYu;M#$K_??%U%)qP1NrdiQm%Bp9X&p zGZHi1mxeT_v*>(tPAI0(&%1qh5mXmP^7eAOwT`x=6+ZQ$d5AG{+wcW$S6i*&3H$Wv z`5{Ce0)`Ock(HgCm*wo&uHc@KVUNx#p)sG-brDlzk#;QGPN23U_sZnaC zR#j1}wMX*web4(o&w2j1=brQV-h2PM9~gp(p8`k;Lom)ksBa_nkjL~uC@>$vI0Qm4 z_Wi}(Fa+4~zap?H2m!wT7wi2kEeP%ZZPC#HDe@7Za~J}247&xP`G5TPI2VXTfN24L z92dsPc*Zj?PJcd1XKPtOK?&&odkfTG#4NWoXi#X?tUX=j%hxaKOs%{99O_IITApR-b2_+u8z$_({pQY9E^_?lJ%Lxw)um4Y z-}4W>TyaKDjqM}}SAe&dg!VeC))bT0_r9A1KjddPnUuKfB_uzsCr>@`W|jzm8wBi2 zg=(C()RGx{{q}{Q*NNO`NfbhXNhfvNgjG)=UuV6*rfXjdScE%>O$|(JLRo~IFRRW% zr}yaEYS^Tuo`*AtuhWGtJ#HnT)zEYf^}pIGyRIgjErR?Qfu^(1j~q`k#9&0P1J9@@ z+hmK$nBT1C7pq7p0U(d2`Hy$5*ci2TO??+zigDpIlV(MW*HSbDr(BCjwn^iy=3YG%{5?~1 z^mA6vDK5$6&1ZM_34*mMsFP65k~#!S6(tAfkBZePTezHVEr=amkhsr5i)<11Gae3Bk^T$|$j+p(xtwof(Xr?DzJhNeGCK{Hl z1;xDO3K9fydzRy!0vKMWfQzli*-XXh7!?(~db&$SCNz7Ysi~}Qj>6u>BRGBE>hk>o zA>Jeq>JuvwDWlVb9h`TLxyGaxC1jpc*-a$F+u$wXlPRVh_fcdip7d$I-q~^ z?fYu@fv%z5#p2d!aotq_y`$v^(6ArP zx-9SL&%-b}Ql1h~x=oQw%-#G`i&4h+`%Xd3!zCj-x1a|9^prRoHXtI#m=xW(OERBL zPv=BdZqLjFI*yH_`x?H@HNwpT#K%fevou_WFK>Pj?dlMLK!hm&>@$W4iaf^kdd3ln z?~(?y6qsAWGL3ZOUFMY7e|57l6z|<0bDrk2YA*2%RRS1jTRdRBQy6KvKJ{y9>L#>k zVe_R@KT!{ryp3h5xQ>lIdY#8u9#k!MVfN=?UW`dCQiT`e^eqL1Ly=0~E&(%-jkf`* zUXcv0b@fFxl|xAUNCvd*Wyio#b%{!vFXYU1>%`BTJbW=#*C`yJ%E6JaE`_==&c81! zAq%_mZ&gyWNPu`L01`0%{~%Tg7SL5oFF!Xv2^j?md3k9WSxH%WSr~%G?%zXDNIrtb z=r7Wz00RCtjsGOT|MDv2f4qtj_c{!@r!sE|pJP>kj#x?dQuh9@SECR#ROR4)eD!xU zmF_n!<@W?u(uurVa|N;uO#2=|H+w7BytQ>HQ|Mn<(PdtX=Jo9<$0|6?qa0t^Ef+Jv?(Axl<-+C456ved zR14F?S`FFst?(9SG0(4*s|4aeA0jKukSBwW*0|8l)UfI!+RNx&_J zz2Rb1E<0TAmx|C@^AwfqZOo=Ow|tLZlYCH(Ocy@07fV=?AX1A}sM@So2n-ejNh{za zNdeN7VkVA2ca!ndk`Nr)fR*pgHp!TEt=18Ra$N`q_!d6t(!Q;-S+@@qXrBcC2tP?I zcY78bkD5Y@aZ1tLOoOFg-2?z!&6as`)H5}FfQNWrff#?8`c6OXBkQtzsrFbxc-u{- z%U+3{K=xegE17<_a`svc_Br0Q(KMUyFw++~>qr~xdrcfON?KX5x-cb6)3L#_umt``m+@GX3x#xk76gtdGdFU@;+FUCW*8N4U|VRut3 zA#=yPdh=+=7pIitM}mgP?uxw7ewn7|6JDSl!$h?+v>F>AwbFSj@tS#X>b0uvS&i`> zRpqQ1^oAmfL)E4L73r+7qcU7ZAyF^eu{3YtpzQ}?P0hSy(nYM5bv|d zeoZ{s{UUAEwI7b9?@KIaaU|3O6Vua-BOI@k`u(oWI=vV^_gyq;eTP&q=f+3M<=1>) z<9+}S?FkPM$UL!}Cr=b%*f^6DX=n;l{kcm46ETz;kNr49ySXy)KFaNWo|Z8RT6iL|fI z7`o{Dxz^=Sa&4|ubn)RcGwn_YvMkf^L=<7&BhwDD-e~*xnXSP&1=wZUN_E@ci=C}iX+uXZheqa; zrwmARHL0=K%8NS=#f9TIi)~Lv51*%d^L=Jf>=GesJIa&_E2Q9eBk<_PP1Y~ySWT8I z{wR{5Cya~H{jSg;2CZ3YrxWV!V9n!;K+IVS@^A(OaqCk_=VgCOpTZ*{+$0M4c)d*R z17QO=_jXw$^$k@Dg>iilz)Edw)<*ksVmjSDA2V`XzciGfm>$i$GizT{zNDr)J=`I5 z#%>GrHxH%9-Bfy~U*f9^ojKA_W`lF`m zT2u5w7e3aU^rp||+QhMau98H14r5+6*Osvvh4DLWDgOQ5pK9);@O|+8H5oP5y)pVD zzRAPucT{}O9n#W6MlQLf+^J8b;Uboj#x8!3>AitD!nA{4Du_;@X2;Vb6*Rm z!)>z%a9Jt8`d=pZ!qw>vk=f>*R$nWtiQhBE3q1C6pTsk7q~SLcDXMz#S zCu|Q=1c6!Lf+=@2BLh2p`S_U6-8{}i)<-G}Le|LP7EJN(M1qF&j~M2EtZf(<`pl^T z0_lAsz4NN&03~9IKSaS;4CboG(4!m!uBpn)OL^TI!xOe#=E@!XKtS)j#Z#_chPypbCeM1p|8}V{N)?udQ~g4u2_y-OtCbBlEm@ z=B;BQ+MZi03LuiTU^n4fx8UVJgNEJq#)AdR;# zD=Yn+s18l|A;ir*P@;xz)FW-Gdl^sZ22MO-{>mhE_L-O>4+rNC-f$eHCyQd-c_2E8 z56)%*A|^cJWLli}5?N^X8T`8uw&}pDC9eDaI<%jCPM4S>{iUUk0ko-xJ2Z)Pmu_;p zDDVG>csN>oSMEWhNN58}CFopuPuu+ZK)g*N6rTN%m_mgdGU=Y5?7$hZ6S4&Vh^6ij zKZ;Te%gjZdvA2~6Oesu`GqY^i2fQY~0y6b569&#KZcq);iCGs_V5I{m011&}CL>Zt zN54kE>8k|OvjU3oxNBqD(6ivJ%H|W6OsvIP*MtcheTyTG3RDmtjPN@k9+R<92h1Rz zNp7nYRyVxyT`f_o!13 zROazdppozUl0TfnRtGJb8jqP!CA%c{d7Lx%wJt1$Wt6(`*O6Uw z_nExHrAOA#ou8}BLOi9h953j3I}f>3qvRgmMsij4`!FIr2(?hnL#MT#aD}l()@Wn! zIwrc8$%!QS>Q$&Dz)wKXW#RH&;ku6d;0EE$O!32LHt(}9(#subeacy90=p%`om%M# zPP3|&r?iqDXM@vUA8^OpE_`UQ99#&UG_X1cG@ksC!23|we$FP^QMU0&O-M_XjY|3N z$0!;s)c!te%hfR7Aft2m5GJ$5Bj~y>j6iarDu!pGb7O`+;=N&JtE1I7ot?2ah@z=h zW2K$ZPl4%!zuON?jp_ux)7X81p9@7<=7o)muQ!`VU*(U_GMZtu2O?fDpn4;&s;Q(e zRGDC(^{49^Z4ZPC^eY6YNJjucoEOc70koBr&K)(r+s?@g^Se%SH`l9uWoVU9k8~!7 zSm(Da`qf`K>*l$GR3fX#>*?3KSLGipLS>5eZQT02fellR^9>>##$oNHE}<85p%~Q7 z%_(}1jZoEc^nl$(=_+s4AiZQK^f5uJuh zyx{G`iKeIyY%fRaL#e4KJl~qBe?H-g4xGk|wmuFpt&b6U)UaK7xbX7Trn9x!O|I0d zM0C&$Crky#52J>FMHwh5IKcoA3);!)%GXyMs9+@{1qF^WmEsdjJ%}afv*WKBoMu}7 U@+;F6i_J(6+nRj}N+2@-U-0Veh5!Hn literal 0 HcmV?d00001 diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index e4d71f97710..67c7cb3cd63 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -29,7 +29,6 @@ import java.util.logging.Logger; import io.helidon.common.buffers.BufferData; -import io.helidon.common.tls.Tls; import io.helidon.http.Header; import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; @@ -59,11 +58,11 @@ /** * A gRPC client call handler. The typical order of calls will be: - * + *

* start (request | sendMessage)* (halfClose | cancel) * - * @param - * @param + * @param request type + * @param response type */ class GrpcClientCall extends ClientCall { private static final Logger LOGGER = Logger.getLogger(GrpcClientCall.class.getName()); @@ -71,6 +70,8 @@ class GrpcClientCall extends ClientCall { private static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); + private static final int READ_TIMEOUT_SECONDS = 10; + private static final int BUFFER_SIZE_BYTES = 1024; private static final int WAIT_TIME_MILLIS = 100; private static final Duration WAIT_TIME_MILLIS_DURATION = Duration.ofMillis(WAIT_TIME_MILLIS); @@ -135,7 +136,7 @@ public int priority() { @Override public Duration readTimeout() { - return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(10)); + return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(READ_TIMEOUT_SECONDS)); } }, null, // Http2ClientConfig @@ -181,7 +182,7 @@ public void halfClose() { @Override public void sendMessage(ReqT message) { LOGGER.finest("sendMessage called"); - BufferData messageData = BufferData.growing(512); + BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); messageData.readFrom(requestMarshaller.stream(message)); BufferData headerData = BufferData.create(5); headerData.writeInt8(0); // no compression @@ -297,13 +298,12 @@ private ClientConnection clientConnection() { ClientUri clientUri = clientConfig.baseUri().orElseThrow(); WebClient webClient = grpcClient.webClient(); - Tls tls = Tls.builder().enabled(false).build(); ConnectionKey connectionKey = new ConnectionKey( clientUri.scheme(), clientUri.host(), clientUri.port(), clientConfig.readTimeout().orElse(Duration.ZERO), - tls, + clientConfig.tls(), DefaultDnsResolver.create(), DnsAddressLookup.defaultLookup(), Proxy.noProxy()); From 993b5c4945adf74029567a2a5c866a787f797f14 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Fri, 23 Feb 2024 10:44:21 -0500 Subject: [PATCH 12/38] Simplified and optimized implementation of gRPC unary calls. --- .../src/main/resources/logging.properties | 4 +- .../webclient/grpc/GrpcBaseClientCall.java | 209 ++++++++++++++++++ .../helidon/webclient/grpc/GrpcChannel.java | 7 +- .../io/helidon/webclient/grpc/GrpcClient.java | 6 +- .../webclient/grpc/GrpcClientCall.java | 185 ++-------------- .../webclient/grpc/GrpcClientImpl.java | 3 +- .../grpc/GrpcClientMethodDescriptor.java | 1 + .../grpc/GrpcProtocolConfigProvider.java | 2 +- .../webclient/grpc/GrpcServiceClientImpl.java | 4 +- .../webclient/grpc/GrpcUnaryClientCall.java | 125 +++++++++++ 10 files changed, 369 insertions(+), 177 deletions(-) create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java create mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index 74f705d03b4..4f88e74fd60 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -19,5 +19,5 @@ java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$ # Global logging level. Can be overridden by specific loggers .level=INFO io.helidon.webserver.level=INFO -io.helidon.webclient.grpc.level=FINEST -io.helidon.webserver.grpc.level=FINEST +#io.helidon.webclient.grpc.level=FINEST +#io.helidon.webserver.grpc.level=FINEST diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java new file mode 100644 index 00000000000..9ae40f7e56d --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -0,0 +1,209 @@ +/* + * 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.webclient.grpc; + +import java.io.InputStream; +import java.time.Duration; +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import io.helidon.common.buffers.BufferData; +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderValues; +import io.helidon.http.WritableHeaders; +import io.helidon.http.http2.Http2Headers; +import io.helidon.http.http2.Http2Settings; +import io.helidon.http.http2.Http2StreamState; +import io.helidon.webclient.api.ClientConnection; +import io.helidon.webclient.api.ClientUri; +import io.helidon.webclient.api.ConnectionKey; +import io.helidon.webclient.api.DefaultDnsResolver; +import io.helidon.webclient.api.DnsAddressLookup; +import io.helidon.webclient.api.Proxy; +import io.helidon.webclient.api.TcpClientConnection; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.http2.Http2ClientConnection; +import io.helidon.webclient.http2.Http2ClientImpl; +import io.helidon.webclient.http2.Http2StreamConfig; + +import io.grpc.CallOptions; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; + +/** + * Base class for gRPC client calls. + */ +abstract class GrpcBaseClientCall extends ClientCall { + private static final Logger LOGGER = Logger.getLogger(GrpcBaseClientCall.class.getName()); + + protected static final Metadata EMPTY_METADATA = new Metadata(); + protected static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); + protected static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); + + protected static final int READ_TIMEOUT_SECONDS = 10; + protected static final int BUFFER_SIZE_BYTES = 1024; + protected static final int WAIT_TIME_MILLIS = 100; + protected static final Duration WAIT_TIME_MILLIS_DURATION = Duration.ofMillis(WAIT_TIME_MILLIS); + + protected static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); + + private final GrpcClientImpl grpcClient; + private final MethodDescriptor methodDescriptor; + private final CallOptions callOptions; + + private final MethodDescriptor.Marshaller requestMarshaller; + private final MethodDescriptor.Marshaller responseMarshaller; + + private volatile Http2ClientConnection connection; + private volatile GrpcClientStream clientStream; + private volatile Listener responseListener; + + GrpcBaseClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { + this.grpcClient = grpcClient; + this.methodDescriptor = methodDescriptor; + this.callOptions = callOptions; + this.requestMarshaller = methodDescriptor.getRequestMarshaller(); + this.responseMarshaller = methodDescriptor.getResponseMarshaller(); + } + + public Http2ClientConnection connection() { + return connection; + } + + public MethodDescriptor.Marshaller requestMarshaller() { + return requestMarshaller; + } + + public GrpcClientStream clientStream() { + return clientStream; + } + + public Listener responseListener() { + return responseListener; + } + + @Override + public void start(Listener responseListener, Metadata metadata) { + LOGGER.finest("start called"); + + this.responseListener = responseListener; + + // obtain HTTP2 connection + ClientConnection clientConnection = clientConnection(); + connection = Http2ClientConnection.create((Http2ClientImpl) grpcClient.http2Client(), + clientConnection, true); + + // create HTTP2 stream from connection + clientStream = new GrpcClientStream( + connection, + Http2Settings.create(), // Http2Settings + clientConnection.helidonSocket(), // SocketContext + new Http2StreamConfig() { + @Override + public boolean priorKnowledge() { + return true; + } + + @Override + public int priority() { + return 0; + } + + @Override + public Duration readTimeout() { + return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(READ_TIMEOUT_SECONDS)); + } + }, + null, // Http2ClientConfig + connection.streamIdSequence()); + + // start streaming threads + startStreamingThreads(); + + // send HEADERS frame + ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); + WritableHeaders headers = WritableHeaders.create(); + headers.add(Http2Headers.AUTHORITY_NAME, clientUri.authority()); + headers.add(Http2Headers.METHOD_NAME, "POST"); + headers.add(Http2Headers.PATH_NAME, "/" + methodDescriptor.getFullMethodName()); + headers.add(Http2Headers.SCHEME_NAME, "http"); + headers.add(GRPC_CONTENT_TYPE); + headers.add(GRPC_ACCEPT_ENCODING); + clientStream.writeHeaders(Http2Headers.create(headers), false); + } + + abstract void startStreamingThreads(); + + /** + * Unary blocking calls that use stubs provide their own executor which needs + * to be used at least once to unblock the calling thread and complete the + * gRPC invocation. This method submits an empty task for that purpose. There + * may be a better way to achieve this. + */ + protected void unblockUnaryExecutor() { + Executor executor = callOptions.getExecutor(); + if (executor != null) { + try { + executor.execute(() -> {}); + } catch (Throwable t) { + // ignored + } + } + } + + protected ClientConnection clientConnection() { + GrpcClientConfig clientConfig = grpcClient.prototype(); + ClientUri clientUri = clientConfig.baseUri().orElseThrow(); + WebClient webClient = grpcClient.webClient(); + + ConnectionKey connectionKey = new ConnectionKey( + clientUri.scheme(), + clientUri.host(), + clientUri.port(), + clientConfig.readTimeout().orElse(Duration.ZERO), + clientConfig.tls(), + DefaultDnsResolver.create(), + DnsAddressLookup.defaultLookup(), + Proxy.noProxy()); + + return TcpClientConnection.create(webClient, + connectionKey, + Collections.emptyList(), + connection -> false, + connection -> { + }).connect(); + } + + protected boolean isRemoteOpen() { + return clientStream.streamState() != Http2StreamState.HALF_CLOSED_REMOTE + && clientStream.streamState() != Http2StreamState.CLOSED; + } + + protected ResT toResponse(BufferData bufferData) { + bufferData.read(); // compression + bufferData.readUnsignedInt32(); // length prefixed + return responseMarshaller.parse(new InputStream() { + @Override + public int read() { + return bufferData.available() > 0 ? bufferData.read() : -1; + } + }); + } +} 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 80767f1cce7..ee1ea90f3da 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 @@ -35,14 +35,17 @@ class GrpcChannel extends Channel { * * @param grpcClient the gRPC client */ - public GrpcChannel(GrpcClient grpcClient) { + GrpcChannel(GrpcClient grpcClient) { this.grpcClient = (GrpcClientImpl) grpcClient; } @Override public ClientCall newCall( MethodDescriptor methodDescriptor, CallOptions callOptions) { - return new GrpcClientCall<>(grpcClient, methodDescriptor, callOptions); + MethodDescriptor.MethodType methodType = methodDescriptor.getType(); + return methodType == MethodDescriptor.MethodType.UNARY + ? new GrpcUnaryClientCall<>(grpcClient, methodDescriptor, callOptions) + : new GrpcClientCall<>(grpcClient, methodDescriptor, callOptions); } @Override 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 d86950b215d..c7b1e1795bc 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 @@ -18,16 +18,20 @@ import java.util.function.Consumer; -import io.grpc.Channel; import io.helidon.builder.api.RuntimeType; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.spi.Protocol; +import io.grpc.Channel; + /** * gRPC client. */ @RuntimeType.PrototypedBy(GrpcClientConfig.class) public interface GrpcClient extends RuntimeType.Api { + /** + * Protocol ID constant for gRPC. + */ String PROTOCOL_ID = "grpc"; /** diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 67c7cb3cd63..d6fa87781f6 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -16,11 +16,7 @@ package io.helidon.webclient.grpc; -import java.io.InputStream; -import java.time.Duration; -import java.util.Collections; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; @@ -29,134 +25,41 @@ import java.util.logging.Logger; import io.helidon.common.buffers.BufferData; -import io.helidon.http.Header; -import io.helidon.http.HeaderNames; -import io.helidon.http.HeaderValues; -import io.helidon.http.WritableHeaders; import io.helidon.http.http2.Http2FrameData; -import io.helidon.http.http2.Http2Headers; -import io.helidon.http.http2.Http2Settings; -import io.helidon.http.http2.Http2StreamState; -import io.helidon.webclient.api.ClientConnection; -import io.helidon.webclient.api.ClientUri; -import io.helidon.webclient.api.ConnectionKey; -import io.helidon.webclient.api.DefaultDnsResolver; -import io.helidon.webclient.api.DnsAddressLookup; -import io.helidon.webclient.api.Proxy; -import io.helidon.webclient.api.TcpClientConnection; -import io.helidon.webclient.api.WebClient; -import io.helidon.webclient.http2.Http2ClientConnection; -import io.helidon.webclient.http2.Http2ClientImpl; -import io.helidon.webclient.http2.Http2StreamConfig; import io.helidon.webclient.http2.StreamTimeoutException; import io.grpc.CallOptions; -import io.grpc.ClientCall; -import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; /** - * A gRPC client call handler. The typical order of calls will be: + * * An implementation of a gRPC call. Expects: *

* start (request | sendMessage)* (halfClose | cancel) * * @param request type * @param response type */ -class GrpcClientCall extends ClientCall { +class GrpcClientCall extends GrpcBaseClientCall { private static final Logger LOGGER = Logger.getLogger(GrpcClientCall.class.getName()); - private static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); - private static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); - - private static final int READ_TIMEOUT_SECONDS = 10; - private static final int BUFFER_SIZE_BYTES = 1024; - private static final int WAIT_TIME_MILLIS = 100; - private static final Duration WAIT_TIME_MILLIS_DURATION = Duration.ofMillis(WAIT_TIME_MILLIS); - - private static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); - private final ExecutorService executor; - private final GrpcClientImpl grpcClient; - private final MethodDescriptor methodDescriptor; - private final CallOptions callOptions; private final AtomicInteger messageRequest = new AtomicInteger(); - private final MethodDescriptor.Marshaller requestMarshaller; - private final MethodDescriptor.Marshaller responseMarshaller; - private final LinkedBlockingQueue sendingQueue = new LinkedBlockingQueue<>(); private final LinkedBlockingQueue receivingQueue = new LinkedBlockingQueue<>(); private final CountDownLatch startReadBarrier = new CountDownLatch(1); private final CountDownLatch startWriteBarrier = new CountDownLatch(1); - private volatile Http2ClientConnection connection; - private volatile GrpcClientStream clientStream; - private volatile Listener responseListener; private volatile Future readStreamFuture; private volatile Future writeStreamFuture; GrpcClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { - this.grpcClient = grpcClient; - this.methodDescriptor = methodDescriptor; - this.callOptions = callOptions; - this.requestMarshaller = methodDescriptor.getRequestMarshaller(); - this.responseMarshaller = methodDescriptor.getResponseMarshaller(); + super(grpcClient, methodDescriptor, callOptions); this.executor = grpcClient.webClient().executor(); } - @Override - public void start(Listener responseListener, Metadata metadata) { - LOGGER.finest("start called"); - - this.responseListener = responseListener; - - // obtain HTTP2 connection - ClientConnection clientConnection = clientConnection(); - connection = Http2ClientConnection.create((Http2ClientImpl) grpcClient.http2Client(), - clientConnection, true); - - // create HTTP2 stream from connection - clientStream = new GrpcClientStream( - connection, - Http2Settings.create(), // Http2Settings - clientConnection.helidonSocket(), // SocketContext - new Http2StreamConfig() { - @Override - public boolean priorKnowledge() { - return true; - } - - @Override - public int priority() { - return 0; - } - - @Override - public Duration readTimeout() { - return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(READ_TIMEOUT_SECONDS)); - } - }, - null, // Http2ClientConfig - connection.streamIdSequence()); - - // start streaming threads - startStreamingThreads(); - - // send HEADERS frame - ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); - WritableHeaders headers = WritableHeaders.create(); - headers.add(Http2Headers.AUTHORITY_NAME, clientUri.authority()); - headers.add(Http2Headers.METHOD_NAME, "POST"); - headers.add(Http2Headers.PATH_NAME, "/" + methodDescriptor.getFullMethodName()); - headers.add(Http2Headers.SCHEME_NAME, "http"); - headers.add(GRPC_CONTENT_TYPE); - headers.add(GRPC_ACCEPT_ENCODING); - clientStream.writeHeaders(Http2Headers.create(headers), false); - } - @Override public void request(int numMessages) { LOGGER.finest(() -> "request called " + numMessages); @@ -167,7 +70,7 @@ public void request(int numMessages) { @Override public void cancel(String message, Throwable cause) { LOGGER.finest(() -> "cancel called " + message); - responseListener.onClose(Status.CANCELLED, new Metadata()); + responseListener().onClose(Status.CANCELLED, EMPTY_METADATA); readStreamFuture.cancel(true); writeStreamFuture.cancel(true); close(); @@ -183,7 +86,7 @@ public void halfClose() { public void sendMessage(ReqT message) { LOGGER.finest("sendMessage called"); BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); - messageData.readFrom(requestMarshaller.stream(message)); + messageData.readFrom(requestMarshaller().stream(message)); BufferData headerData = BufferData.create(5); headerData.writeInt8(0); // no compression headerData.writeUnsignedInt32(messageData.available()); // length prefixed @@ -191,7 +94,7 @@ public void sendMessage(ReqT message) { startWriteBarrier.countDown(); } - private void startStreamingThreads() { + protected void startStreamingThreads() { // write streaming thread writeStreamFuture = executor.submit(() -> { try { @@ -207,14 +110,14 @@ private void startStreamingThreads() { LOGGER.finest("[Writing thread] sending queue end marker found"); if (!endOfStream) { LOGGER.finest("[Writing thread] sending empty buffer to end stream"); - clientStream.writeData(EMPTY_BUFFER_DATA, true); + clientStream().writeData(EMPTY_BUFFER_DATA, true); } break; } endOfStream = (sendingQueue.peek() == EMPTY_BUFFER_DATA); boolean lastEndOfStream = endOfStream; LOGGER.finest(() -> "[Writing thread] writing bufferData " + lastEndOfStream); - clientStream.writeData(bufferData, endOfStream); + clientStream().writeData(bufferData, endOfStream); } } } catch (Throwable e) { @@ -230,14 +133,14 @@ private void startStreamingThreads() { LOGGER.fine("[Reading thread] started"); // read response headers - clientStream.readHeaders(); + clientStream().readHeaders(); while (isRemoteOpen()) { // drain queue drainReceivingQueue(); // trailers received? - if (clientStream.trailers().isDone()) { + if (clientStream().trailers().isDone()) { LOGGER.finest("[Reading thread] trailers received"); break; } @@ -245,7 +148,7 @@ private void startStreamingThreads() { // attempt to read and queue Http2FrameData frameData; try { - frameData = clientStream.readOne(WAIT_TIME_MILLIS_DURATION); + frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { LOGGER.fine("[Reading thread] read timeout"); continue; @@ -257,10 +160,10 @@ private void startStreamingThreads() { } LOGGER.finest("[Reading thread] closing listener"); - responseListener.onClose(Status.OK, new Metadata()); + responseListener().onClose(Status.OK, EMPTY_METADATA); } catch (Throwable e) { LOGGER.finest(e.getMessage()); - responseListener.onClose(Status.UNKNOWN, new Metadata()); + responseListener().onClose(Status.UNKNOWN, EMPTY_METADATA); } finally { close(); } @@ -271,74 +174,18 @@ private void startStreamingThreads() { private void close() { LOGGER.finest("closing client call"); sendingQueue.clear(); - clientStream.cancel(); - connection.close(); + clientStream().cancel(); + connection().close(); unblockUnaryExecutor(); } - /** - * Unary blocking calls that use stubs provide their own executor which needs - * to be used at least once to unblock the calling thread and complete the - * gRPC invocation. This method submits an empty task for that purpose. There - * may be a better way to achieve this. - */ - private void unblockUnaryExecutor() { - Executor executor = callOptions.getExecutor(); - if (executor != null) { - try { - executor.execute(() -> {}); - } catch (Throwable t) { - // ignored - } - } - } - - private ClientConnection clientConnection() { - GrpcClientConfig clientConfig = grpcClient.prototype(); - ClientUri clientUri = clientConfig.baseUri().orElseThrow(); - WebClient webClient = grpcClient.webClient(); - - ConnectionKey connectionKey = new ConnectionKey( - clientUri.scheme(), - clientUri.host(), - clientUri.port(), - clientConfig.readTimeout().orElse(Duration.ZERO), - clientConfig.tls(), - DefaultDnsResolver.create(), - DnsAddressLookup.defaultLookup(), - Proxy.noProxy()); - - return TcpClientConnection.create(webClient, - connectionKey, - Collections.emptyList(), - connection -> false, - connection -> { - }).connect(); - } - - private boolean isRemoteOpen() { - return clientStream.streamState() != Http2StreamState.HALF_CLOSED_REMOTE - && clientStream.streamState() != Http2StreamState.CLOSED; - } - - private ResT toResponse(BufferData bufferData) { - bufferData.read(); // compression - bufferData.readUnsignedInt32(); // length prefixed - return responseMarshaller.parse(new InputStream() { - @Override - public int read() { - return bufferData.available() > 0 ? bufferData.read() : -1; - } - }); - } - private void drainReceivingQueue() { LOGGER.finest("[Reading thread] draining receiving queue"); while (messageRequest.get() > 0 && !receivingQueue.isEmpty()) { messageRequest.getAndDecrement(); ResT res = toResponse(receivingQueue.remove()); LOGGER.finest("[Reading thread] sending response to listener"); - responseListener.onMessage(res); + responseListener().onMessage(res); } } } 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 903dcfb2657..ec66b7cc28a 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 @@ -16,10 +16,11 @@ package io.helidon.webclient.grpc; -import io.grpc.Channel; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.http2.Http2Client; +import io.grpc.Channel; + class GrpcClientImpl implements GrpcClient { private final WebClient webClient; private final Http2Client http2Client; diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index 3c35e380477..c5c43470d8b 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -200,6 +200,7 @@ public String name() { * @param the response type * @return The {@link io.grpc.MethodDescriptor} of this method. */ + @SuppressWarnings("unchecked") public MethodDescriptor descriptor() { return (MethodDescriptor) descriptor; } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java index a6b43a5ba63..010ba59b466 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java @@ -23,9 +23,9 @@ * Implementation of protocol config provider for gRPC. */ public class GrpcProtocolConfigProvider implements ProtocolConfigProvider { + /** * Required to be used by {@link java.util.ServiceLoader}. - * @deprecated do not use directly, use Http1ClientProtocol */ public GrpcProtocolConfigProvider() { } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index c51a46e4a9d..3b31b7d0200 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -157,6 +157,8 @@ private ClientCall ensureMethod(String methodName, Meth throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + ", yet " + methodType + " was requested."); } - return new GrpcClientCall<>(grpcClient, method.descriptor(), CallOptions.DEFAULT); + return methodType == MethodDescriptor.MethodType.UNARY + ? new GrpcUnaryClientCall<>(grpcClient, method.descriptor(), CallOptions.DEFAULT) + : new GrpcClientCall<>(grpcClient, method.descriptor(), CallOptions.DEFAULT); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java new file mode 100644 index 00000000000..ab5e13dcb6c --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -0,0 +1,125 @@ +/* + * 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.webclient.grpc; + +import java.util.logging.Logger; + +import io.helidon.common.buffers.BufferData; +import io.helidon.http.http2.Http2FrameData; +import io.helidon.webclient.http2.StreamTimeoutException; + +import io.grpc.CallOptions; +import io.grpc.MethodDescriptor; +import io.grpc.Status; + +/** + * An implementation of a unary gRPC call. Expects: + *

+ * start request sendMessage (halfClose | cancel) + * + * @param request type + * @param response type + */ +class GrpcUnaryClientCall extends GrpcBaseClientCall { + private static final Logger LOGGER = Logger.getLogger(GrpcUnaryClientCall.class.getName()); + + private volatile boolean closeCalled; + private volatile boolean requestSent; + private volatile boolean responseSent; + + GrpcUnaryClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, + CallOptions callOptions) { + super(grpcClient, methodDescriptor, callOptions); + } + + @Override + public void request(int numMessages) { + LOGGER.finest(() -> "request called " + numMessages); + if (numMessages < 1) { + close(Status.INVALID_ARGUMENT); + } + } + + @Override + public void cancel(String message, Throwable cause) { + LOGGER.finest(() -> "cancel called " + message); + close(Status.CANCELLED); + } + + @Override + public void halfClose() { + LOGGER.finest("halfClose called"); + close(responseSent ? Status.OK : Status.UNKNOWN); + } + + @Override + public void sendMessage(ReqT message) { + LOGGER.finest("sendMessage called"); + + // should only be called once + if (requestSent) { + close(Status.FAILED_PRECONDITION); + return; + } + + BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); + messageData.readFrom(requestMarshaller().stream(message)); + BufferData headerData = BufferData.create(5); + headerData.writeInt8(0); // no compression + headerData.writeUnsignedInt32(messageData.available()); // length prefixed + clientStream().writeData(BufferData.create(headerData, messageData), true); + requestSent = true; + + while (isRemoteOpen()) { + // trailers received? + if (clientStream().trailers().isDone()) { + LOGGER.finest("trailers received"); + return; + } + + // attempt to read and queue + Http2FrameData frameData; + try { + frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); + } catch (StreamTimeoutException e) { + LOGGER.fine("read timeout"); + continue; + } + if (frameData != null) { + LOGGER.finest("response received"); + responseListener().onMessage(toResponse(frameData.data())); + responseSent = true; + } + } + } + + @Override + protected void startStreamingThreads() { + // no-op + } + + private void close(Status status) { + if (!closeCalled) { + LOGGER.finest("closing client call"); + responseListener().onClose(status, EMPTY_METADATA); + clientStream().cancel(); + connection().close(); + unblockUnaryExecutor(); + closeCalled = true; + } + } +} From 1391a36c8228c94c89b2ffa74d05212a676cc0dc Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 26 Feb 2024 10:51:48 -0500 Subject: [PATCH 13/38] Moved test to webclient module. --- examples/webserver/protocols/pom.xml | 48 +----- .../webserver/protocols/ProtocolsMain.java | 14 +- .../protocols/src/main/proto/strings.proto | 1 + .../src/main/resources/logging.properties | 9 +- webclient/tests/grpc/pom.xml | 148 ++++++++++++++++++ .../tests/grpc/src/main/proto/events.proto | 60 +++++++ .../tests/grpc/src/main/proto/strings.proto | 30 ++++ .../webclient/grpc/tests}/GrpcStubTest.java | 6 +- .../webclient/grpc/tests}/GrpcTest.java | 4 +- .../tests/grpc}/src/test/resources/client.p12 | Bin .../tests/grpc}/src/test/resources/server.p12 | Bin webclient/tests/pom.xml | 1 + 12 files changed, 254 insertions(+), 67 deletions(-) create mode 100644 webclient/tests/grpc/pom.xml create mode 100644 webclient/tests/grpc/src/main/proto/events.proto create mode 100644 webclient/tests/grpc/src/main/proto/strings.proto rename {examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols => webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests}/GrpcStubTest.java (98%) rename {examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols => webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests}/GrpcTest.java (99%) rename {examples/webserver/protocols => webclient/tests/grpc}/src/test/resources/client.p12 (100%) rename {examples/webserver/protocols => webclient/tests/grpc}/src/test/resources/server.p12 (100%) diff --git a/examples/webserver/protocols/pom.xml b/examples/webserver/protocols/pom.xml index 8eeec4ab408..67bd3b2bd79 100644 --- a/examples/webserver/protocols/pom.xml +++ b/examples/webserver/protocols/pom.xml @@ -44,41 +44,21 @@ io.helidon.webserver helidon-webserver - - io.helidon.webserver - helidon-webserver-http2 - - - io.helidon.webserver - helidon-webserver-websocket - io.helidon.webserver helidon-webserver-grpc - - io.helidon.webclient - helidon-webclient - io.helidon.webclient helidon-webclient-http2 - io.helidon.webclient - helidon-webclient-websocket - - - io.helidon.webclient - helidon-webclient-grpc - - - io.helidon.logging - helidon-logging-common + io.helidon.webserver + helidon-webserver-websocket - io.helidon.logging - helidon-logging-jul + io.helidon.webserver + helidon-webserver-http2 org.junit.jupiter @@ -90,26 +70,6 @@ hamcrest-all test - - io.helidon.webserver.testing.junit5 - helidon-webserver-testing-junit5 - test - - - io.helidon.webserver.testing.junit5 - helidon-webserver-testing-junit5-http2 - test - - - io.helidon.webserver.testing.junit5 - helidon-webserver-testing-junit5-websocket - test - - - io.helidon.webserver.testing.junit5 - helidon-webserver-testing-junit5-grpc - test - diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java index af7e5e83bae..e8010bacc8f 100644 --- a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -79,23 +79,13 @@ public static void main(String[] args) { .unary(Strings.getDescriptor(), "StringService", "Upper", - ProtocolsMain::grpcUpper) - .unary(Strings.getDescriptor(), - "StringService", - "Upper", - ProtocolsMain::blockingGrpcUpper)) + ProtocolsMain::grpcUpper)) .addRouting(WsRouting.builder() .endpoint("/tyrus/echo", ProtocolsMain::wsEcho)) .build() .start(); } - private static Strings.StringMessage blockingGrpcUpper(Strings.StringMessage reqT) { - return Strings.StringMessage.newBuilder() - .setText(reqT.getText().toUpperCase(Locale.ROOT)) - .build(); - } - private static void grpcUpper(Strings.StringMessage request, StreamObserver observer) { String requestText = request.getText(); System.out.println("grpc request: " + requestText); diff --git a/examples/webserver/protocols/src/main/proto/strings.proto b/examples/webserver/protocols/src/main/proto/strings.proto index 756b2246797..d1a4f7be178 100644 --- a/examples/webserver/protocols/src/main/proto/strings.proto +++ b/examples/webserver/protocols/src/main/proto/strings.proto @@ -20,6 +20,7 @@ option java_package = "io.helidon.examples.grpc.strings"; 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) {} diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index 4f88e74fd60..d09df1098a3 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, 2024 Oracle and/or its affiliates. +# Copyright (c) 2022, 2023 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. @@ -13,11 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -handlers=io.helidon.logging.jul.HelidonConsoleHandler -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n - +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n # Global logging level. Can be overridden by specific loggers .level=INFO io.helidon.webserver.level=INFO -#io.helidon.webclient.grpc.level=FINEST -#io.helidon.webserver.grpc.level=FINEST diff --git a/webclient/tests/grpc/pom.xml b/webclient/tests/grpc/pom.xml new file mode 100644 index 00000000000..34d4c34cf32 --- /dev/null +++ b/webclient/tests/grpc/pom.xml @@ -0,0 +1,148 @@ + + + + 4.0.0 + + io.helidon.webclient.tests + helidon-webclient-tests-project + 4.0.0-SNAPSHOT + + + helidon-webclient-tests-grpc + Helidon WebClient gRPC Tests + + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver + helidon-webserver-http2 + + + io.helidon.webserver + helidon-webserver-websocket + + + io.helidon.webserver + helidon-webserver-grpc + + + io.helidon.webclient + helidon-webclient + + + io.helidon.webclient + helidon-webclient-http2 + + + io.helidon.webclient + helidon-webclient-websocket + + + io.helidon.webclient + helidon-webclient-grpc + + + io.helidon.logging + helidon-logging-common + + + io.helidon.logging + helidon-logging-jul + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-http2 + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-websocket + test + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5-grpc + test + + + + + + + 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 + 0.6.1 + + + + compile + compile-custom + + + + + com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} + + + + + + diff --git a/webclient/tests/grpc/src/main/proto/events.proto b/webclient/tests/grpc/src/main/proto/events.proto new file mode 100644 index 00000000000..4690d200aa6 --- /dev/null +++ b/webclient/tests/grpc/src/main/proto/events.proto @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 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.webclient.grpc.tests"; + +import "google/protobuf/empty.proto"; + +service EventService { + rpc Send (Message) returns (google.protobuf.Empty) {} + rpc Events (stream EventRequest) returns (stream EventResponse) {} +} + +message Message { + string text = 2; +} + +message EventRequest { + int64 id = 1; + enum Action { + SUBSCRIBE = 0; + UNSUBSCRIBE = 1; + } + Action action = 2; +} + +message EventResponse { + oneof response_type { + Subscribed subscribed = 1; + Unsubscribed unsubscribed = 2; + Event event = 3; + } +} + +message Subscribed { + int64 id = 1; +} + +message Unsubscribed { + int64 id = 1; +} + +message Event { + int64 id = 1; + string text = 2; +} diff --git a/webclient/tests/grpc/src/main/proto/strings.proto b/webclient/tests/grpc/src/main/proto/strings.proto new file mode 100644 index 00000000000..2da17d2f5de --- /dev/null +++ b/webclient/tests/grpc/src/main/proto/strings.proto @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 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.webclient.grpc.tests"; + +service StringService { + rpc Upper (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/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java similarity index 98% rename from examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java rename to webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index 43640d20bcc..2adfa07779c 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.examples.webserver.protocols; +package io.helidon.webclient.grpc.tests; import java.util.ArrayList; import java.util.Iterator; @@ -28,8 +28,8 @@ import io.grpc.stub.StreamObserver; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; -import io.helidon.examples.grpc.strings.StringServiceGrpc; -import io.helidon.examples.grpc.strings.Strings; +import io.helidon.webclient.grpc.tests.StringServiceGrpc; +import io.helidon.webclient.grpc.tests.Strings; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webserver.WebServer; diff --git a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java similarity index 99% rename from examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java rename to webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java index a8c3c0a093e..8c24972f0e8 100644 --- a/examples/webserver/protocols/src/test/java/io/helidon/examples/webserver/protocols/GrpcTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.examples.webserver.protocols; +package io.helidon.webclient.grpc.tests; import java.util.ArrayList; import java.util.Iterator; @@ -29,7 +29,7 @@ import io.grpc.stub.StreamObserver; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; -import io.helidon.examples.grpc.strings.Strings; +import io.helidon.webclient.grpc.tests.Strings; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; import io.helidon.webclient.grpc.GrpcServiceDescriptor; diff --git a/examples/webserver/protocols/src/test/resources/client.p12 b/webclient/tests/grpc/src/test/resources/client.p12 similarity index 100% rename from examples/webserver/protocols/src/test/resources/client.p12 rename to webclient/tests/grpc/src/test/resources/client.p12 diff --git a/examples/webserver/protocols/src/test/resources/server.p12 b/webclient/tests/grpc/src/test/resources/server.p12 similarity index 100% rename from examples/webserver/protocols/src/test/resources/server.p12 rename to webclient/tests/grpc/src/test/resources/server.p12 diff --git a/webclient/tests/pom.xml b/webclient/tests/pom.xml index b4a829ffef0..a8febb0986d 100644 --- a/webclient/tests/pom.xml +++ b/webclient/tests/pom.xml @@ -37,6 +37,7 @@ http1 http2 webclient + grpc From 2365caf89be99af41fcc9611fde301fad613bcc0 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 26 Feb 2024 13:03:36 -0500 Subject: [PATCH 14/38] Removed currently unused types from grpc core module. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/ProtocolsMain.java | 2 +- .../src/main/resources/logging.properties | 2 +- .../io/helidon/grpc/core/ContextKeys.java | 52 -- .../helidon/grpc/core/GrpcTracingContext.java | 45 -- .../io/helidon/grpc/core/GrpcTracingName.java | 32 -- .../grpc/core/InterceptorPriorities.java | 2 +- .../helidon/grpc/core/MarshallerSupplier.java | 2 +- .../io/helidon/grpc/core/MethodHandler.java | 2 +- .../io/helidon/grpc/core/PriorityBag.java | 2 +- .../io/helidon/grpc/core/ResponseHelper.java | 455 ------------------ .../helidon/grpc/core/SafeStreamObserver.java | 169 ------- .../io/helidon/grpc/core/package-info.java | 4 +- 12 files changed, 8 insertions(+), 761 deletions(-) delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java index e8010bacc8f..07e7c5adfa6 100644 --- a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index d09df1098a3..161f6db0dde 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# 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. diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java b/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java deleted file mode 100644 index f51358e14e4..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/ContextKeys.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2019, 2021 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.grpc.core; - -import java.lang.reflect.Method; - -import io.grpc.Context; -import io.grpc.Metadata; - -/** - * A collection of common gRPC {@link Context.Key} and - * {@link Metadata.Key} instances. - */ -public final class ContextKeys { - /** - * The {@link Metadata.Key} to use to obtain the authorization data. - */ - public static final Metadata.Key AUTHORIZATION = - Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); - - /** - * The gRPC context key to use to obtain the Helidon {@link io.helidon.common.context.Context} - * from the gRPC {@link Context}. - */ - public static final Context.Key HELIDON_CONTEXT = - Context.key(io.helidon.common.context.Context.class.getCanonicalName()); - - /** - * The {@link Context.Key} to use to obtain the actual underlying rpc {@link Method}. - */ - public static final Context.Key SERVICE_METHOD = Context.key(Method.class.getName()); - - /** - * Private constructor for utility class. - */ - private ContextKeys() { - } -} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java deleted file mode 100644 index e310ed43937..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingContext.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022 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.grpc.core; - -import java.util.Optional; - -import io.grpc.Context; -import io.helidon.tracing.Span; - -/** - * Contextual information related to Tracing. - */ -public final class GrpcTracingContext { - private static final String SPAN_KEY_NAME = "io.helidon.tracing.active-span"; - - /** - * Context key for Span instance. - */ - public static final Context.Key SPAN_KEY = Context.key(SPAN_KEY_NAME); - - /** - * Get the current active span associated with the context. - * - * @return span if one is in current context - */ - public static Optional activeSpan() { - return Optional.ofNullable(SPAN_KEY.get()); - } - - private GrpcTracingContext() { - } -} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java b/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java deleted file mode 100644 index 1a48b0f2f8a..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/GrpcTracingName.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2022 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.grpc.core; - -import io.grpc.MethodDescriptor; - -/** - * Name generator for span operation name. - */ -@FunctionalInterface -public interface GrpcTracingName { - /** - * Constructs a span's operation name from the gRPC method. - * - * @param method method to extract a name from - * @return operation name - */ - String name(MethodDescriptor method); -} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java index d4da4008e66..5e6bd13784e 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java index 47e14278f76..4c179eb7c0f 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java b/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java index 205152293f5..fd898b0e1a2 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java index 7114abb16d2..a85fda66d97 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java b/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java deleted file mode 100644 index 3878c748664..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/ResponseHelper.java +++ /dev/null @@ -1,455 +0,0 @@ -/* - * Copyright (c) 2019, 2021 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.grpc.core; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import io.grpc.stub.StreamObserver; - -/** - * A number of helper methods to handle sending responses to a {@link StreamObserver}. - */ -public final class ResponseHelper { - private ResponseHelper() { - } - - /** - * Complete a gRPC request. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * specified value then calling {@link StreamObserver#onCompleted()}. - * - * @param observer the {@link StreamObserver} to complete - * @param value the value to use when calling {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void complete(StreamObserver observer, T value) { - StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); - safe.onNext(value); - safe.onCompleted(); - } - - /** - * Complete a gRPC request based on the result of a {@link CompletionStage}. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * result obtained on completion of the specified {@link CompletionStage} and then calling - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} - * will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param future the {@link CompletionStage} to use to obtain the value to use to call - * {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void complete(StreamObserver observer, CompletionStage future) { - future.whenComplete(completeWithResult(observer)); - } - - /** - * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * result obtained on completion of the specified {@link CompletionStage} and then calling - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} - * will be called. - *

- * The execution will take place asynchronously on the fork-join thread pool. - * - * @param observer the {@link StreamObserver} to complete - * @param future the {@link CompletionStage} to use to obtain the value to use to call - * {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void completeAsync(StreamObserver observer, CompletionStage future) { - future.whenCompleteAsync(completeWithResult(observer)); - } - - /** - * Asynchronously complete a gRPC request based on the result of a {@link CompletionStage}. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * result obtained on completion of the specified {@link CompletionStage} and then calling - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} - * will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param future the {@link CompletionStage} to use to obtain the value to use to call - * {@link StreamObserver#onNext(Object)} - * @param executor the {@link Executor} on which to execute the asynchronous - * request completion - * @param they type of the request result - */ - public static void completeAsync(StreamObserver observer, CompletionStage future, Executor executor) { - future.whenCompleteAsync(completeWithResult(observer), executor); - } - - /** - * Complete a gRPC request based on the result of a {@link Callable}. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * result obtained on completion of the specified {@link Callable} and then calling - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} - * will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param callable the {@link Callable} to use to obtain the value to use to call - * {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void complete(StreamObserver observer, Callable callable) { - try { - observer.onNext(callable.call()); - observer.onCompleted(); - } catch (Throwable t) { - observer.onError(t); - } - } - - /** - * Asynchronously complete a gRPC request based on the result of a {@link Callable}. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * result obtained on completion of the specified {@link Callable} and then calling - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} - * will be called. - *

- * The execution will take place asynchronously on the fork-join thread pool. - * - * @param observer the {@link StreamObserver} to complete - * @param callable the {@link Callable} to use to obtain the value to use to call - * {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void completeAsync(StreamObserver observer, Callable callable) { - completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable))); - } - - /** - * Asynchronously complete a gRPC request based on the result of a {@link Callable}. - *

- * The request will be completed by calling {@link StreamObserver#onNext(Object)} using the - * result obtained on completion of the specified {@link Callable} and then calling - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link Callable#call()} method throws an exception then {@link StreamObserver#onError(Throwable)} - * will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param callable the {@link Callable} to use to obtain the value to use to call - * {@link StreamObserver#onNext(Object)} - * @param executor the {@link Executor} on which to execute the asynchronous - * request completion - * @param they type of the request result - */ - public static void completeAsync(StreamObserver observer, Callable callable, Executor executor) { - completeAsync(observer, CompletableFuture.supplyAsync(createSupplier(callable), executor)); - } - - /** - * Execute a {@link Runnable} task and on completion of the task complete the gRPC request by - * calling {@link StreamObserver#onNext(Object)} using the specified result and then call - * {@link StreamObserver#onCompleted()}. - *

- * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} - * will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param task the {@link Runnable} to execute - * @param result the result to pass to {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void complete(StreamObserver observer, Runnable task, T result) { - complete(observer, Executors.callable(task, result)); - } - - /** - * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC - * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then - * call {@link StreamObserver#onCompleted()}. - *

- * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} - * will be called. - *

- * The task and and request completion will be executed on the fork-join thread pool. - * - * @param observer the {@link StreamObserver} to complete - * @param task the {@link Runnable} to execute - * @param result the result to pass to {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void completeAsync(StreamObserver observer, Runnable task, T result) { - completeAsync(observer, Executors.callable(task, result)); - } - - /** - * Asynchronously execute a {@link Runnable} task and on completion of the task complete the gRPC - * request by calling {@link StreamObserver#onNext(Object)} using the specified result and then - * call {@link StreamObserver#onCompleted()}. - *

- * If the {@link Runnable#run()} method throws an exception then {@link StreamObserver#onError(Throwable)} - * will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param task the {@link Runnable} to execute - * @param result the result to pass to {@link StreamObserver#onNext(Object)} - * @param executor the {@link Executor} on which to execute the asynchronous - * request completion - * @param they type of the request result - */ - public static void completeAsync(StreamObserver observer, Runnable task, T result, Executor executor) { - completeAsync(observer, Executors.callable(task, result), executor); - } - - /** - * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the - * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. - *

- * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void stream(StreamObserver observer, Stream stream) { - stream(observer, () -> stream); - } - - /** - * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until - * the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. - *

- * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param stream the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} - * @param executor the {@link Executor} on which to execute the asynchronous - * request completion - * @param they type of the request result - */ - public static void streamAsync(StreamObserver observer, Stream stream, Executor executor) { - executor.execute(() -> stream(observer, () -> stream)); - } - - /** - * Send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method until the - * {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. - *

- * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} - * @param they type of the request result - */ - public static void stream(StreamObserver observer, Supplier> supplier) { - StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); - Throwable thrown = null; - - try { - supplier.get().forEach(safe::onNext); - } catch (Throwable t) { - thrown = t; - } - - if (thrown == null) { - safe.onCompleted(); - } else { - safe.onError(thrown); - } - } - - /** - * Asynchronously send the values from a {@link Stream} to the {@link StreamObserver#onNext(Object)} method - * until the {@link Stream} is exhausted call {@link StreamObserver#onCompleted()}. - *

- * If an error occurs whilst streaming results then {@link StreamObserver#onError(Throwable)} will be called. - * - * @param observer the {@link StreamObserver} to complete - * @param supplier the {@link Supplier} of the {@link Stream} of results to send to {@link StreamObserver#onNext(Object)} - * @param executor the {@link Executor} on which to execute the asynchronous - * request completion - * @param they type of the request result - */ - public static void streamAsync(StreamObserver observer, Supplier> supplier, Executor executor) { - executor.execute(() -> stream(observer, supplier)); - } - - - /** - * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until - * the {@link CompletionStage} completes then call {@link StreamObserver#onCompleted()}. - *

- * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} - * will be called instead of {@link StreamObserver#onCompleted()}. - * - * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes - * @param stage the {@link CompletionStage} to await completion of - * @param they type of the request result - * - * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method - */ - // todo: a bit of a chicken or egg when used with Coherence streaming methods, isn't it? - public static Consumer stream(StreamObserver observer, CompletionStage stage) { - StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); - stage.whenComplete(completeWithoutResult(safe)); - return safe::onNext; - } - - /** - * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until - * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the - * fork-join thread pool. - *

- * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} - * will be called instead of {@link StreamObserver#onCompleted()}. - * - * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes - * @param stage the {@link CompletionStage} to await completion of - * @param they type of the request result - * - * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method - */ - public static Consumer streamAsync(StreamObserver observer, CompletionStage stage) { - StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); - stage.whenCompleteAsync(completeWithoutResult(safe)); - return value -> CompletableFuture.runAsync(() -> safe.onNext(value)); - } - - /** - * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method until - * the {@link CompletionStage} completes then asynchronously call {@link StreamObserver#onCompleted()} using the executor - * thread. - *

- * If the {@link CompletionStage} completes with an error then {@link StreamObserver#onError(Throwable)} - * will be called instead of {@link StreamObserver#onCompleted()}. - * - * @param observer the {@link StreamObserver} to send values to and complete when the {@link CompletionStage} completes - * @param stage the {@link CompletionStage} to await completion of - * @param executor the {@link Executor} on which to execute the asynchronous - * request completion - * @param they type of the request result - * - * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method - */ - public static Consumer streamAsync(StreamObserver observer, CompletionStage stage, Executor executor) { - StreamObserver safe = SafeStreamObserver.ensureSafeObserver(observer); - stage.whenCompleteAsync(completeWithoutResult(safe), executor); - return value -> CompletableFuture.runAsync(() -> safe.onNext(value), executor); - } - - /** - * Obtain a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method. - * @param observer the {@link StreamObserver} to complete - * @param the type of the result - * @param the type of the response - * @return a {@link Consumer} that can be used to send values to the {@link StreamObserver#onNext(Object)} method - */ - public static BiConsumer completeWithResult(StreamObserver observer) { - return new CompletionAction<>(observer, true); - } - - /** - * Obtain a {@link Consumer} that can be used to complete a {@link StreamObserver}. - * @param observer the {@link StreamObserver} to complete - * @param the type of the response - * @return a {@link Consumer} that can be used to complete a {@link StreamObserver} - */ - public static BiConsumer completeWithoutResult(StreamObserver observer) { - return new CompletionAction<>(observer, false); - } - - /** - * Convert a {@link Callable} to a {@link Supplier}. - * @param callable the {@link Callable} to convert - * @param the result returned by the {@link Callable} - * @return a {@link Supplier} that wraps the {@link Callable} - */ - public static Supplier createSupplier(Callable callable) { - return new CallableSupplier<>(callable); - } - - /** - * A {@link BiConsumer} that is used to handle completion of a - * {@link CompletionStage} by forwarding - * the result to a {@link io.grpc.stub.StreamObserver}. - * - * @param the type of the {@link CompletionStage}'s result - * @param the type of result expected by the {@link io.grpc.stub.StreamObserver} - */ - private static class CompletionAction implements BiConsumer { - private StreamObserver observer; - private boolean sendResult; - - CompletionAction(StreamObserver observer, boolean sendResult) { - this.observer = observer; - this.sendResult = sendResult; - } - - @Override - @SuppressWarnings("unchecked") - public void accept(T result, Throwable error) { - if (error != null) { - observer.onError(error); - } else { - if (sendResult) { - observer.onNext((U) result); - } - observer.onCompleted(); - } - } - } - - /** - * A class that converts a {@link Callable} to a {@link Supplier}. - * @param the type of result returned from the callable - */ - private static class CallableSupplier implements Supplier { - private Callable callable; - - CallableSupplier(Callable callable) { - this.callable = callable; - } - - @Override - public T get() { - try { - return callable.call(); - } catch (Exception e) { - throw new CompletionException(e.getMessage(), e); - } - } - } -} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java b/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java deleted file mode 100644 index 9661a45be3f..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/SafeStreamObserver.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2019, 2021 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.grpc.core; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import io.grpc.Status; -import io.grpc.stub.StreamObserver; - -/** - * A {@link io.grpc.stub.StreamObserver} that handles exceptions correctly. - * - * @param the type of response expected - */ -public class SafeStreamObserver - implements StreamObserver { - - /** - * Create a {@link SafeStreamObserver} that wraps - * another {@link io.grpc.stub.StreamObserver}. - * - * @param streamObserver the {@link io.grpc.stub.StreamObserver} to wrap - */ - private SafeStreamObserver(StreamObserver streamObserver) { - delegate = streamObserver; - } - - @Override - public void onNext(T t) { - if (done) { - return; - } - - if (t == null) { - onError(Status.INVALID_ARGUMENT - .withDescription("onNext called with null. Null values are generally not allowed.") - .asRuntimeException()); - } else { - try { - delegate.onNext(t); - } catch (Throwable thrown) { - throwIfFatal(thrown); - onError(thrown); - } - } - } - - @Override - public void onError(Throwable thrown) { - try { - if (done) { - LOGGER.log(Level.SEVERE, checkNotNull(thrown), () -> "OnError called after StreamObserver was closed"); - } else { - done = true; - delegate.onError(checkNotNull(thrown)); - } - } catch (Throwable t) { - throwIfFatal(t); - LOGGER.log(Level.SEVERE, t, () -> "Caught exception handling onError"); - } - } - - @Override - public void onCompleted() { - if (done) { - LOGGER.log(Level.WARNING, "onComplete called after StreamObserver was closed"); - } else { - try { - done = true; - delegate.onCompleted(); - } catch (Throwable thrown) { - throwIfFatal(thrown); - LOGGER.log(Level.SEVERE, thrown, () -> "Caught exception handling onComplete"); - } - } - } - - /** - * Obtain the wrapped {@link StreamObserver}. - * @return the wrapped {@link StreamObserver} - */ - public StreamObserver delegate() { - return delegate; - } - - private Throwable checkNotNull(Throwable thrown) { - if (thrown == null) { - thrown = Status.INVALID_ARGUMENT - .withDescription("onError called with null Throwable. Null exceptions are generally not allowed.") - .asRuntimeException(); - } - - return thrown; - } - - /** - * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These varieties are - * as follows: - *

    - *
  • {@code VirtualMachineError}
  • - *
  • {@code ThreadDeath}
  • - *
  • {@code LinkageError}
  • - *
- * - * @param thrown the {@code Throwable} to test and perhaps throw - */ - private static void throwIfFatal(Throwable thrown) { - if (thrown instanceof VirtualMachineError) { - throw (VirtualMachineError) thrown; - } else if (thrown instanceof ThreadDeath) { - throw (ThreadDeath) thrown; - } else if (thrown instanceof LinkageError) { - throw (LinkageError) thrown; - } - } - - /** - * Ensure that the specified {@link StreamObserver} is a safe observer. - *

- * If the specified observer is not an instance of {@link SafeStreamObserver} then wrap - * it in a {@link SafeStreamObserver}. - * - * @param observer the {@link StreamObserver} to test - * @param the response type expected by the observer - * - * @return a safe {@link StreamObserver} - */ - public static StreamObserver ensureSafeObserver(StreamObserver observer) { - if (observer instanceof SafeStreamObserver) { - return observer; - } - - return new SafeStreamObserver<>(observer); - } - - // ----- constants ------------------------------------------------------ - - /** - * The {2link Logger} to use. - */ - private static final Logger LOGGER = Logger.getLogger(SafeStreamObserver.class.getName()); - - // ----- data members --------------------------------------------------- - - /** - * The actual StreamObserver. - */ - private StreamObserver delegate; - - /** - * Indicates a terminal state. - */ - private boolean done; -} 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 019189e5db5..a10d014bfed 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * 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. @@ -15,6 +15,6 @@ */ /** - * Core classes used by both the reactive gRPC server API and gRPC client API. + * Core classes used by both the gRPC server API and gRPC client API. */ package io.helidon.grpc.core; From 46aa20c6327a27a555519361bdf5cbfbc114a1ac Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 26 Feb 2024 13:32:41 -0500 Subject: [PATCH 15/38] Cleanup of webserver gRPC module. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/ProtocolsMain.java | 2 +- .../src/main/resources/logging.properties | 2 +- grpc/core/pom.xml | 4 - grpc/core/src/main/java/module-info.java | 1 - .../helidon/webclient/websocket/WsClient.java | 2 +- .../java/io/helidon/webserver/grpc/Grpc.java | 11 +- .../webserver/grpc/GrpcProtocolSelector.java | 2 +- .../helidon/webserver/grpc/GrpcRouting.java | 19 --- .../webserver/grpc/GrpcServerCalls.java | 137 ------------------ .../helidon/webserver/grpc/GrpcService.java | 6 +- .../webserver/grpc/GrpcServiceRoute.java | 22 +-- .../testing/junit5/grpc/package-info.java | 20 +++ .../java/io/helidon/webserver/RouterImpl.java | 2 +- 13 files changed, 28 insertions(+), 202 deletions(-) delete mode 100644 webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java create mode 100644 webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/package-info.java diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java index 07e7c5adfa6..e8010bacc8f 100644 --- a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index 161f6db0dde..d09df1098a3 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, 2024 Oracle and/or its affiliates. +# Copyright (c) 2022, 2023 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. diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml index def59c84858..7c91bc3569c 100644 --- a/grpc/core/pom.xml +++ b/grpc/core/pom.xml @@ -37,10 +37,6 @@ io.helidon.common helidon-common-config - - io.helidon.tracing - helidon-tracing - io.grpc grpc-api diff --git a/grpc/core/src/main/java/module-info.java b/grpc/core/src/main/java/module-info.java index a46489ed6ad..656c779f2ce 100644 --- a/grpc/core/src/main/java/module-info.java +++ b/grpc/core/src/main/java/module-info.java @@ -28,7 +28,6 @@ requires transitive io.grpc.protobuf.lite; requires java.logging; - requires io.helidon.tracing; requires jakarta.inject; requires jakarta.annotation; diff --git a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java index 007dd200af1..b382572585c 100644 --- a/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java +++ b/webclient/websocket/src/main/java/io/helidon/webclient/websocket/WsClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java index f9ff3d2eb4a..7ed1caedd9f 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/Grpc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -54,15 +54,6 @@ static Grpc unary(Descriptors.FileDescriptor proto, return grpc(proto, serviceName, methodName, ServerCalls.asyncUnaryCall(method)); } - static Grpc unary(Descriptors.FileDescriptor proto, - String serviceName, - String methodName, - GrpcServerCalls.Unary method) { - - return grpc(proto, serviceName, methodName, GrpcServerCalls.unaryCall(method)); - } - - static Grpc bidi(Descriptors.FileDescriptor proto, String serviceName, String methodName, diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java index c625af21e65..806e08bf767 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcProtocolSelector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java index 86ee4f5027a..24fa0ce724c 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcRouting.java @@ -133,25 +133,6 @@ public Builder unary(Descriptors.FileDescriptor proto, return route(Grpc.unary(proto, serviceName, methodName, method)); } - /** - * Unary route. - * - * @param proto proto descriptor - * @param serviceName service name - * @param methodName method name - * @param method method to handle this route - * @param request type - * @param response type - * @return updated builder - */ - public Builder unary(Descriptors.FileDescriptor proto, - String serviceName, - String methodName, - GrpcServerCalls.Unary method) { - - return route(Grpc.unary(proto, serviceName, methodName, method)); - } - /** * Bidirectional route. * diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java deleted file mode 100644 index c5e72a39b54..00000000000 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServerCalls.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.helidon.webserver.grpc; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -import io.grpc.ServerCallHandler; -import io.grpc.stub.ServerCalls; -import io.grpc.stub.StreamObserver; - -public final class GrpcServerCalls { - private GrpcServerCalls() { - } - - static ServerCallHandler unaryCall(Unary method) { - return ServerCalls.asyncUnaryCall((request, responseObserver) -> { - try { - ResT response = method.invoke(request); - responseObserver.onNext(response); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError(e); - } - }); - } - - static ServerCallHandler clientStream(ClientStream method, Duration timeout) { - return ServerCalls.asyncClientStreamingCall(responseObserver -> { - CompletableFuture> future = new CompletableFuture<>(); - - future.orTimeout(timeout.getNano(), TimeUnit.NANOSECONDS) - .thenAccept(requests -> { - responseObserver.onNext(method.invoke(requests)); - responseObserver.onCompleted(); - }) - .exceptionally(throwable -> { - responseObserver.onError(throwable); - return null; - }); - - return new CollectingObserver<>(future); - }); - } - - static ServerCallHandler serverStream(ServerStream method) { - return ServerCalls.asyncServerStreamingCall((request, responseObserver) -> { - try { - Collection response = method.invoke(request); - for (ResT resT : response) { - responseObserver.onNext(resT); - } - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError(e); - } - }); - } - - static ServerCallHandler bidi(Bidi method, Duration timeout) { - return ServerCalls.asyncBidiStreamingCall(responseObserver -> { - CompletableFuture> future = new CompletableFuture<>(); - - future.orTimeout(timeout.getNano(), TimeUnit.NANOSECONDS) - .thenAccept(requests -> { - Collection response = method.invoke(requests); - response.forEach(responseObserver::onNext); - responseObserver.onCompleted(); - }) - .exceptionally(throwable -> { - responseObserver.onError(throwable); - return null; - }); - - return new CollectingObserver<>(future); - }); - } - - public interface Unary { - RespT invoke(ReqT request); - } - - public interface ServerStream { - Collection invoke(ReqT request); - } - - public interface ClientStream { - RespT invoke(Collection requests); - } - - /** - * Bidirectional streaming is by its design created for asynchronous communication. - * This interface should be used only when you have a guarantee that the client sends all of its messages - * and DOES NOT WAIT for the responses on each of them. - *

- * In case you need true asynchronous communication (e.g. clients sends a message, waits for server response, - * send another one), - * please use {@link io.grpc.stub.ServerCalls#asyncBidiStreamingCall(io.grpc.stub.ServerCalls.BidiStreamingMethod)}. - * - * @param request type - * @param response type - */ - public interface Bidi { - Collection invoke(Collection requests); - } - - /** - * Collects all elements (and possible exception) and completes the completable future when finished collecting. - * - * @param - */ - private static class CollectingObserver implements StreamObserver { - private final List collectedValues = new ArrayList<>(); - private final CompletableFuture> future; - - private CollectingObserver(CompletableFuture> future) { - this.future = future; - } - - @Override - public void onNext(T value) { - collectedValues.add(value); - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onCompleted() { - future.complete(collectedValues); - } - } -} diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java index a41f661a3a9..50926ebd66e 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -60,7 +60,6 @@ interface Routing { * @return updated routing */ Routing unary(String methodName, ServerCalls.UnaryMethod method); - Routing unary(String methodName, GrpcServerCalls.Unary method); /** * Bidirectional route. @@ -72,7 +71,6 @@ interface Routing { * @return updated routing */ Routing bidi(String methodName, ServerCalls.BidiStreamingMethod method); - Routing bidi(String methodName, GrpcServerCalls.Bidi method); /** * Server streaming route. @@ -84,7 +82,6 @@ interface Routing { * @return updated routing */ Routing serverStream(String methodName, ServerCalls.ServerStreamingMethod method); - Routing serverStream(String methodName, GrpcServerCalls.ServerStream method); /** * Client streaming route. @@ -96,6 +93,5 @@ interface Routing { * @return updated routing */ Routing clientStream(String methodName, ServerCalls.ClientStreamingMethod method); - Routing clientStream(String methodName, GrpcServerCalls.ClientStream method); } } diff --git a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java index 5c553223872..fdd0d91e76b 100644 --- a/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java +++ b/webserver/grpc/src/main/java/io/helidon/webserver/grpc/GrpcServiceRoute.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. @@ -78,22 +78,12 @@ public GrpcService.Routing unary(String methodName, ServerCalls.Una return this; } - @Override - public GrpcService.Routing unary(String methodName, GrpcServerCalls.Unary method) { - return null; - } - @Override public GrpcService.Routing bidi(String methodName, ServerCalls.BidiStreamingMethod method) { routes.add(Grpc.bidi(proto, serviceName, methodName, method)); return this; } - @Override - public GrpcService.Routing bidi(String methodName, GrpcServerCalls.Bidi method) { - return null; - } - @Override public GrpcService.Routing serverStream(String methodName, ServerCalls.ServerStreamingMethod method) { @@ -101,11 +91,6 @@ public GrpcService.Routing serverStream(String methodName, return this; } - @Override - public GrpcService.Routing serverStream(String methodName, GrpcServerCalls.ServerStream method) { - return null; - } - @Override public GrpcService.Routing clientStream(String methodName, ServerCalls.ClientStreamingMethod method) { @@ -113,11 +98,6 @@ public GrpcService.Routing clientStream(String methodName, return this; } - @Override - public GrpcService.Routing clientStream(String methodName, GrpcServerCalls.ClientStream method) { - return null; - } - public GrpcServiceRoute build() { return new GrpcServiceRoute(serviceName, List.copyOf(routes)); } diff --git a/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/package-info.java b/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/package-info.java new file mode 100644 index 00000000000..cc425f5e775 --- /dev/null +++ b/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/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. + */ + +/** + * Helidon WebServer Testing JUnit 5 Support for gRPC. + */ +package io.helidon.webserver.testing.junit5.grpc; diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java b/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java index 026c652366d..c0b5df7020e 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/RouterImpl.java @@ -84,7 +84,7 @@ public RouterImpl build() { public Router.Builder addRouting(io.helidon.common.Builder routing) { var previous = this.routings.put(routing.getClass(), routing); if (previous != null) { - // Thread.dumpStack(); + Thread.dumpStack(); LOGGER.log(System.Logger.Level.WARNING, "Second routing of the same type is registered. " + "The first instance will be ignored. Type: " + routing.getClass().getName()); } From 5314a162f3d82c80014625b2adc83fd40b67830e Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 26 Feb 2024 15:34:30 -0500 Subject: [PATCH 16/38] Test cleanup and copyright errors. Signed-off-by: Santiago Pericasgeertsen --- .../webserver/protocols/ProtocolsMain.java | 2 +- .../src/main/resources/logging.properties | 2 +- .../webclient/grpc/tests/GrpcBaseTest.java | 142 ++++++++++++++++++ .../webclient/grpc/tests/GrpcStubTest.java | 125 +-------------- .../webclient/grpc/tests/GrpcTest.java | 123 +-------------- 5 files changed, 146 insertions(+), 248 deletions(-) create mode 100644 webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java diff --git a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java index e8010bacc8f..07e7c5adfa6 100644 --- a/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java +++ b/examples/webserver/protocols/src/main/java/io/helidon/examples/webserver/protocols/ProtocolsMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * 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. diff --git a/examples/webserver/protocols/src/main/resources/logging.properties b/examples/webserver/protocols/src/main/resources/logging.properties index d09df1098a3..161f6db0dde 100644 --- a/examples/webserver/protocols/src/main/resources/logging.properties +++ b/examples/webserver/protocols/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# 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. diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java new file mode 100644 index 00000000000..5568fef88b1 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java @@ -0,0 +1,142 @@ +/* + * 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.webclient.grpc.tests; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +import io.grpc.stub.StreamObserver; + +class GrpcBaseTest { + + static void upper(Strings.StringMessage req, + StreamObserver streamObserver) { + Strings.StringMessage msg = Strings.StringMessage.newBuilder() + .setText(req.getText().toUpperCase(Locale.ROOT)) + .build(); + streamObserver.onNext(msg); + streamObserver.onCompleted(); + } + + static void split(Strings.StringMessage req, + StreamObserver streamObserver) { + String[] strings = req.getText().split(" "); + for (String string : strings) { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(string) + .build()); + + } + streamObserver.onCompleted(); + } + + static StreamObserver join(StreamObserver streamObserver) { + return new StreamObserver<>() { + private StringBuilder builder; + + @Override + public void onNext(Strings.StringMessage value) { + if (builder == null) { + builder = new StringBuilder(); + builder.append(value.getText()); + } else { + builder.append(" ").append(value.getText()); + } + } + + @Override + public void onError(Throwable t) { + streamObserver.onError(t); + } + + @Override + public void onCompleted() { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(builder.toString()) + .build()); + streamObserver.onCompleted(); + } + }; + } + + static StreamObserver echo(StreamObserver streamObserver) { + return new StreamObserver<>() { + @Override + public void onNext(Strings.StringMessage value) { + streamObserver.onNext(value); + } + + @Override + public void onError(Throwable t) { + streamObserver.onError(t); + } + + @Override + public void onCompleted() { + streamObserver.onCompleted(); + } + }; + } + + Strings.StringMessage newStringMessage(String data) { + return Strings.StringMessage.newBuilder().setText(data).build(); + } + + static StreamObserver singleStreamObserver(CompletableFuture future) { + return new StreamObserver<>() { + private ReqT value; + + @Override + public void onNext(ReqT value) { + this.value = value; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value); + } + }; + } + + static StreamObserver multiStreamObserver(CompletableFuture> future) { + return new StreamObserver<>() { + private final List value = new ArrayList<>(); + + @Override + public void onNext(ResT value) { + this.value.add(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + future.complete(value.iterator()); + } + }; + } +} diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index 2adfa07779c..198ced7c038 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -16,10 +16,7 @@ package io.helidon.webclient.grpc.tests; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; -import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -28,8 +25,6 @@ import io.grpc.stub.StreamObserver; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; -import io.helidon.webclient.grpc.tests.StringServiceGrpc; -import io.helidon.webclient.grpc.tests.Strings; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webserver.WebServer; @@ -46,7 +41,7 @@ * Tests gRPC client using stubs and TLS. */ @ServerTest -class GrpcStubTest { +class GrpcStubTest extends GrpcBaseTest { private static final long TIMEOUT_SECONDS = 10; private final WebClient webClient; @@ -95,76 +90,6 @@ public static void setup(WebServerConfig.Builder builder) { GrpcStubTest::echo)); } - // -- gRPC server methods -- - - private static Strings.StringMessage upper(Strings.StringMessage reqT) { - return Strings.StringMessage.newBuilder() - .setText(reqT.getText().toUpperCase(Locale.ROOT)) - .build(); - } - - private static void split(Strings.StringMessage reqT, - StreamObserver streamObserver) { - String[] strings = reqT.getText().split(" "); - for (String string : strings) { - streamObserver.onNext(Strings.StringMessage.newBuilder() - .setText(string) - .build()); - - } - streamObserver.onCompleted(); - } - - private static StreamObserver join(StreamObserver streamObserver) { - return new StreamObserver<>() { - private StringBuilder builder; - - @Override - public void onNext(Strings.StringMessage value) { - if (builder == null) { - builder = new StringBuilder(); - builder.append(value.getText()); - } else { - builder.append(" ").append(value.getText()); - } - } - - @Override - public void onError(Throwable t) { - streamObserver.onError(t); - } - - @Override - public void onCompleted() { - streamObserver.onNext(Strings.StringMessage.newBuilder() - .setText(builder.toString()) - .build()); - streamObserver.onCompleted(); - } - }; - } - - private static StreamObserver echo(StreamObserver streamObserver) { - return new StreamObserver<>() { - @Override - public void onNext(Strings.StringMessage value) { - streamObserver.onNext(value); - } - - @Override - public void onError(Throwable t) { - streamObserver.onError(t); - } - - @Override - public void onCompleted() { - streamObserver.onCompleted(); - } - }; - } - - // -- Tests -- - @Test void testUnaryUpper() { GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); @@ -232,52 +157,4 @@ void testBidirectionalEchoAsync() throws ExecutionException, InterruptedExceptio assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } - - // -- Utility methods -- - - private Strings.StringMessage newStringMessage(String data) { - return Strings.StringMessage.newBuilder().setText(data).build(); - } - - private static StreamObserver singleStreamObserver(CompletableFuture future) { - return new StreamObserver<>() { - private ReqT value; - - @Override - public void onNext(ReqT value) { - this.value = value; - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onCompleted() { - future.complete(value); - } - }; - } - - private static StreamObserver multiStreamObserver(CompletableFuture> future) { - return new StreamObserver<>() { - private final List value = new ArrayList<>(); - - @Override - public void onNext(ResT value) { - this.value.add(value); - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onCompleted() { - future.complete(value.iterator()); - } - }; - } } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java index 8c24972f0e8..02ede4430c2 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java @@ -16,10 +16,8 @@ package io.helidon.webclient.grpc.tests; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -29,7 +27,6 @@ import io.grpc.stub.StreamObserver; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; -import io.helidon.webclient.grpc.tests.Strings; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; import io.helidon.webclient.grpc.GrpcServiceDescriptor; @@ -47,7 +44,7 @@ * Tests gRPC client using low-level API and TLS, no stubs. */ @ServerTest -class GrpcTest { +class GrpcTest extends GrpcBaseTest { private static final long TIMEOUT_SECONDS = 10; private final GrpcClient grpcClient; @@ -121,76 +118,6 @@ public static void setup(WebServerConfig.Builder builder) { GrpcTest::echo)); } - // -- gRPC server methods -- - - private static Strings.StringMessage upper(Strings.StringMessage reqT) { - return Strings.StringMessage.newBuilder() - .setText(reqT.getText().toUpperCase(Locale.ROOT)) - .build(); - } - - private static void split(Strings.StringMessage reqT, - StreamObserver streamObserver) { - String[] strings = reqT.getText().split(" "); - for (String string : strings) { - streamObserver.onNext(Strings.StringMessage.newBuilder() - .setText(string) - .build()); - - } - streamObserver.onCompleted(); - } - - private static StreamObserver join(StreamObserver streamObserver) { - return new StreamObserver<>() { - private StringBuilder builder; - - @Override - public void onNext(Strings.StringMessage value) { - if (builder == null) { - builder = new StringBuilder(); - builder.append(value.getText()); - } else { - builder.append(" ").append(value.getText()); - } - } - - @Override - public void onError(Throwable t) { - streamObserver.onError(t); - } - - @Override - public void onCompleted() { - streamObserver.onNext(Strings.StringMessage.newBuilder() - .setText(builder.toString()) - .build()); - streamObserver.onCompleted(); - } - }; - } - - private static StreamObserver echo(StreamObserver streamObserver) { - return new StreamObserver<>() { - @Override - public void onNext(Strings.StringMessage value) { - streamObserver.onNext(value); - } - - @Override - public void onError(Throwable t) { - streamObserver.onError(t); - } - - @Override - public void onCompleted() { - streamObserver.onCompleted(); - } - }; - } - - // -- Tests -- - @Test void testUnaryUpper() { Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) @@ -275,52 +202,4 @@ void testBidirectionalEchoAsync() throws ExecutionException, InterruptedExceptio assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } - - // -- Utility methods -- - - private Strings.StringMessage newStringMessage(String data) { - return Strings.StringMessage.newBuilder().setText(data).build(); - } - - private static StreamObserver singleStreamObserver(CompletableFuture future) { - return new StreamObserver<>() { - private ReqT value; - - @Override - public void onNext(ReqT value) { - this.value = value; - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onCompleted() { - future.complete(value); - } - }; - } - - private static StreamObserver multiStreamObserver(CompletableFuture> future) { - return new StreamObserver<>() { - private final List value = new ArrayList<>(); - - @Override - public void onNext(ResT value) { - this.value.add(value); - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onCompleted() { - future.complete(value.iterator()); - } - }; - } } From 5c09911f521240b3ce38674ce477d572ea0e8133 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Tue, 27 Feb 2024 09:51:05 -0500 Subject: [PATCH 17/38] Adds support for setting up gRPC routes in tests. Updates gRPC tests to implement @SetupRoute methods. --- .../webclient/grpc/tests/GrpcBaseTest.java | 39 +++++++++++++++++++ .../webclient/grpc/tests/GrpcStubTest.java | 36 +---------------- .../webclient/grpc/tests/GrpcTest.java | 39 ++----------------- .../junit5/grpc/GrpcServerExtension.java | 36 +++++++++++++++-- 4 files changed, 77 insertions(+), 73 deletions(-) diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java index 5568fef88b1..234d60a7d37 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java @@ -21,10 +21,49 @@ import java.util.Locale; import java.util.concurrent.CompletableFuture; +import io.helidon.common.configurable.Resource; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.testing.junit5.SetUpRoute; +import io.helidon.webserver.testing.junit5.SetUpServer; + import io.grpc.stub.StreamObserver; class GrpcBaseTest { + @SetUpServer + public static void setup(WebServerConfig.Builder builder) { + builder.tls(tls -> tls.privateKey(key -> key + .keystore(store -> store + .passphrase("password") + .keystore(Resource.create("server.p12")))) + .privateKeyCertChain(key -> key + .keystore(store -> store + .trustStore(true) + .passphrase("password") + .keystore(Resource.create("server.p12"))))); + } + + @SetUpRoute + static void setUpRoute(GrpcRouting.Builder routing) { + routing.unary(Strings.getDescriptor(), + "StringService", + "Upper", + GrpcStubTest::upper) + .serverStream(Strings.getDescriptor(), + "StringService", + "Split", + GrpcStubTest::split) + .clientStream(Strings.getDescriptor(), + "StringService", + "Join", + GrpcStubTest::join) + .bidi(Strings.getDescriptor(), + "StringService", + "Echo", + GrpcStubTest::echo); + } + static void upper(Strings.StringMessage req, StreamObserver streamObserver) { Strings.StringMessage msg = Strings.StringMessage.newBuilder() diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index 198ced7c038..48b93578de4 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -22,18 +22,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import io.grpc.stub.StreamObserver; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; import io.helidon.webclient.api.WebClient; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webserver.WebServer; -import io.helidon.webserver.WebServerConfig; -import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.ServerTest; -import io.helidon.webserver.testing.junit5.SetUpServer; import org.junit.jupiter.api.Test; +import io.grpc.stub.StreamObserver; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -60,36 +58,6 @@ private GrpcStubTest(WebServer server) { .build(); } - @SetUpServer - public static void setup(WebServerConfig.Builder builder) { - builder.tls(tls -> tls.privateKey(key -> key - .keystore(store -> store - .passphrase("password") - .keystore(Resource.create("server.p12")))) - .privateKeyCertChain(key -> key - .keystore(store -> store - .trustStore(true) - .passphrase("password") - .keystore(Resource.create("server.p12"))))) - .addRouting(GrpcRouting.builder() - .unary(Strings.getDescriptor(), - "StringService", - "Upper", - GrpcStubTest::upper) - .serverStream(Strings.getDescriptor(), - "StringService", - "Split", - GrpcStubTest::split) - .clientStream(Strings.getDescriptor(), - "StringService", - "Join", - GrpcStubTest::join) - .bidi(Strings.getDescriptor(), - "StringService", - "Echo", - GrpcStubTest::echo)); - } - @Test void testUnaryUpper() { GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java index 02ede4430c2..ef933137376 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java @@ -23,19 +23,18 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import com.google.protobuf.StringValue; -import io.grpc.stub.StreamObserver; + import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; import io.helidon.webclient.grpc.GrpcServiceDescriptor; import io.helidon.webserver.WebServer; -import io.helidon.webserver.WebServerConfig; -import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.ServerTest; -import io.helidon.webserver.testing.junit5.SetUpServer; + import org.junit.jupiter.api.Test; +import com.google.protobuf.StringValue; +import io.grpc.stub.StreamObserver; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -88,36 +87,6 @@ private GrpcTest(WebServer server) { .build(); } - @SetUpServer - public static void setup(WebServerConfig.Builder builder) { - builder.tls(tls -> tls.privateKey(key -> key - .keystore(store -> store - .passphrase("password") - .keystore(Resource.create("server.p12")))) - .privateKeyCertChain(key -> key - .keystore(store -> store - .trustStore(true) - .passphrase("password") - .keystore(Resource.create("server.p12"))))) - .addRouting(GrpcRouting.builder() - .unary(Strings.getDescriptor(), - "StringService", - "Upper", - GrpcTest::upper) - .serverStream(Strings.getDescriptor(), - "StringService", - "Split", - GrpcTest::split) - .clientStream(Strings.getDescriptor(), - "StringService", - "Join", - GrpcTest::join) - .bidi(Strings.getDescriptor(), - "StringService", - "Echo", - GrpcTest::echo)); - } - @Test void testUnaryUpper() { Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) diff --git a/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java b/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java index 90a44068812..372e35a6ac9 100644 --- a/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java +++ b/webserver/testing/junit5/grpc/src/main/java/io/helidon/webserver/testing/junit5/grpc/GrpcServerExtension.java @@ -16,8 +16,14 @@ package io.helidon.webserver.testing.junit5.grpc; +import java.util.Optional; + import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.ListenerConfig; +import io.helidon.webserver.Router; import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.Junit5Util; import io.helidon.webserver.testing.junit5.spi.ServerJunitExtension; @@ -30,10 +36,13 @@ * artifacts, such as {@link io.helidon.webclient.grpc.GrpcClient} in Helidon integration tests. */ public class GrpcServerExtension implements ServerJunitExtension { - /** - * Required constructor for {@link java.util.ServiceLoader}. - */ - public GrpcServerExtension() { + + @Override + public Optional> setUpRouteParamHandler(Class type) { + if (GrpcRouting.Builder.class.equals(type)) { + return Optional.of(new RoutingParamHandler()); + } + return Optional.empty(); } @Override @@ -56,4 +65,23 @@ public Object resolveParameter(ParameterContext parameterContext, } throw new ParameterResolutionException("gRPC extension only supports GrpcClient parameter type"); } + + private static final class RoutingParamHandler implements ParamHandler { + @Override + public GrpcRouting.Builder get(String socketName, + WebServerConfig.Builder serverBuilder, + ListenerConfig.Builder listenerBuilder, + Router.RouterBuilder routerBuilder) { + return GrpcRouting.builder(); + } + + @Override + public void handle(String socketName, + WebServerConfig.Builder serverBuilder, + ListenerConfig.Builder listenerBuilder, + Router.RouterBuilder routerBuilder, + GrpcRouting.Builder value) { + routerBuilder.addRouting(value); + } + } } From 41c7d2cc584c983710d7965f9e4d0733d8869b81 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Tue, 27 Feb 2024 11:29:16 -0500 Subject: [PATCH 18/38] Removes currently unused types. Signed-off-by: Santiago Pericasgeertsen --- .../webclient/grpc/GrpcClientProtocol.java | 38 ---------------- .../grpc/GrpcProtocolConfigProvider.java | 45 ------------------- webclient/grpc/src/main/java/module-info.java | 2 - 3 files changed, 85 deletions(-) delete mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java delete mode 100644 webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java deleted file mode 100644 index b701edd8f9c..00000000000 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocol.java +++ /dev/null @@ -1,38 +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.webclient.grpc; - -import io.helidon.common.socket.SocketContext; -import io.helidon.http.http2.Http2Settings; -import io.helidon.http.http2.Http2StreamState; -import io.helidon.http.http2.StreamFlowControl; -import io.helidon.webclient.http2.Http2ClientConfig; - -class GrpcClientProtocol { - - private GrpcClientProtocol() { - } - - static GrpcClientStream create(SocketContext scoketContext, - Http2Settings serverSettings, - Http2ClientConfig clientConfig, - int streamId, - StreamFlowControl flowControl, - Http2StreamState streamState) { - return null; - } -} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java deleted file mode 100644 index 010ba59b466..00000000000 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolConfigProvider.java +++ /dev/null @@ -1,45 +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.webclient.grpc; - -import io.helidon.common.config.Config; -import io.helidon.webclient.spi.ProtocolConfigProvider; - -/** - * Implementation of protocol config provider for gRPC. - */ -public class GrpcProtocolConfigProvider implements ProtocolConfigProvider { - - /** - * Required to be used by {@link java.util.ServiceLoader}. - */ - public GrpcProtocolConfigProvider() { - } - - @Override - public String configKey() { - return GrpcProtocolProvider.CONFIG_KEY; - } - - @Override - public GrpcClientProtocolConfig create(Config config, String name) { - return GrpcClientProtocolConfig.builder() - .config(config) - .name(name) - .build(); - } -} diff --git a/webclient/grpc/src/main/java/module-info.java b/webclient/grpc/src/main/java/module-info.java index c16b586450f..26678febb9d 100644 --- a/webclient/grpc/src/main/java/module-info.java +++ b/webclient/grpc/src/main/java/module-info.java @@ -43,6 +43,4 @@ provides io.helidon.webclient.spi.ClientProtocolProvider with io.helidon.webclient.grpc.GrpcProtocolProvider; - provides io.helidon.webclient.spi.ProtocolConfigProvider - with io.helidon.webclient.grpc.GrpcProtocolConfigProvider; } \ No newline at end of file From 1cbf971c53d90c4dda60ec6e14159683449af8ce Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 13 Mar 2024 16:30:19 -0400 Subject: [PATCH 19/38] - Simplifies the grpc.core module to the bare minimum that is needed now - Cleans all module-info.java files - Switches to System.Logger for logging --- grpc/core/pom.xml | 4 - .../grpc/core/InterceptorPriorities.java | 63 ------ .../helidon/grpc/core/MarshallerSupplier.java | 19 +- .../io/helidon/grpc/core/PriorityBag.java | 214 ------------------ grpc/core/src/main/java/module-info.java | 3 - .../webclient/grpc/GrpcBaseClientCall.java | 15 +- .../webclient/grpc/GrpcClientCall.java | 48 ++-- .../grpc/GrpcClientMethodDescriptor.java | 64 +----- .../webclient/grpc/GrpcUnaryClientCall.java | 23 +- webclient/grpc/src/main/java/module-info.java | 2 - 10 files changed, 49 insertions(+), 406 deletions(-) delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java delete mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml index 7c91bc3569c..05b9616a70a 100644 --- a/grpc/core/pom.xml +++ b/grpc/core/pom.xml @@ -59,10 +59,6 @@ - - jakarta.inject - jakarta.inject-api - jakarta.annotation jakarta.annotation-api diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java deleted file mode 100644 index 5e6bd13784e..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorPriorities.java +++ /dev/null @@ -1,63 +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.grpc.core; - -/** - * Constants that represent a priority ordering that interceptors registered with - * a gRPC service or method will be applied. - */ -public class InterceptorPriorities { - /** - * Context priority. - *

- * Interceptors with this priority typically only perform tasks - * such as adding state to the call {@link io.grpc.Context}. - */ - public static final int CONTEXT = 1000; - - /** - * Tracing priority. - *

- * Tracing and metrics interceptors are typically applied after any context - * interceptors so that they can trace and gather metrics on the whole call - * stack of remaining interceptors. - */ - public static final int TRACING = CONTEXT + 1; - - /** - * Security authentication priority. - */ - public static final int AUTHENTICATION = 2000; - - /** - * Security authorization priority. - */ - public static final int AUTHORIZATION = 2000; - - /** - * User-level priority. - * - * This value is also used as a default priority for application-supplied interceptors. - */ - public static final int USER = 5000; - - /** - * Cannot create instances. - */ - private InterceptorPriorities() { - } -} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java index 4c179eb7c0f..a0caade5e36 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -19,7 +19,6 @@ import com.google.protobuf.MessageLite; import io.grpc.MethodDescriptor; import io.grpc.protobuf.lite.ProtoLiteUtils; -import jakarta.inject.Named; /** * A supplier of {@link MethodDescriptor.Marshaller} instances for specific @@ -28,16 +27,6 @@ @FunctionalInterface public interface MarshallerSupplier { - /** - * The name of the Protocol Buffer marshaller supplier. - */ - String PROTO = "proto"; - - /** - * The name to use to specify the default marshaller supplier. - */ - String DEFAULT = "default"; - /** * Obtain a {@link MethodDescriptor.Marshaller} for a type. * @@ -60,9 +49,7 @@ static MarshallerSupplier defaultInstance() { /** * The default {@link MarshallerSupplier}. */ - @Named(MarshallerSupplier.DEFAULT) - class DefaultMarshallerSupplier - implements MarshallerSupplier { + class DefaultMarshallerSupplier implements MarshallerSupplier { private final ProtoMarshallerSupplier proto = new ProtoMarshallerSupplier(); @@ -82,9 +69,7 @@ public MethodDescriptor.Marshaller get(Class clazz) { * A {@link MarshallerSupplier} implementation that * supplies Protocol Buffer marshaller instances. */ - @Named(PROTO) - class ProtoMarshallerSupplier - implements MarshallerSupplier { + class ProtoMarshallerSupplier implements MarshallerSupplier { @Override @SuppressWarnings("unchecked") diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java deleted file mode 100644 index a85fda66d97..00000000000 --- a/grpc/core/src/main/java/io/helidon/grpc/core/PriorityBag.java +++ /dev/null @@ -1,214 +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.grpc.core; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Stream; - -// import io.helidon.common.Prioritized; -import jakarta.annotation.Priority; - -/** - * A bag of values ordered by priority. - *

- * An element with lower priority number is more significant than an element - * with a higher priority number. - *

- * For cases where priority is the same, elements are ordered in the order that - * they were added to the bag. - *

- * Elements added with negative priorities are assumed to have no priority and - * will be least significant in order. - * - * @param the type of elements in the bag - */ -public class PriorityBag implements Iterable { - - private final Map> contents; - - private final List noPriorityList; - - private final int defaultPriority; - - private PriorityBag(Map> contents, List noPriorityList, int defaultPriority) { - this.contents = contents; - this.noPriorityList = noPriorityList; - this.defaultPriority = defaultPriority; - } - - /** - * Create a new {@link PriorityBag} where elements - * added with no priority will be last in the order. - * - * @param the type of elements in the bag - * @return a new {@link PriorityBag} where elements - * dded with no priority will be last in the - * order - */ - public static PriorityBag create() { - return new PriorityBag<>(new TreeMap<>(), new ArrayList<>(), -1); - } - - /** - * Create a new {@link PriorityBag} where elements - * added with no priority will be be given a default - * priority value. - * - * @param priority the default priority value to assign - * to elements added with no priority - * @param the type of elements in the bag - * - * @return a new {@link PriorityBag} where elements - * added with no priority will be be given - * a default priority value - */ - public static PriorityBag withDefaultPriority(int priority) { - return new PriorityBag<>(new TreeMap<>(), new ArrayList<>(), priority); - } - - - /** - * Obtain a copy of this {@link PriorityBag}. - * - * @return a copy of this {@link PriorityBag} - */ - public PriorityBag copyMe() { - PriorityBag copy = PriorityBag.create(); - copy.merge(this); - return copy; - } - - /** - * Obtain an immutable copy of this {@link PriorityBag}. - * - * @return an immutable copy of this {@link PriorityBag} - */ - public PriorityBag readOnly() { - return new PriorityBag<>(Collections.unmodifiableMap(contents), - Collections.unmodifiableList(noPriorityList), - defaultPriority); - } - - /** - * Merge a {@link PriorityBag} into this {@link PriorityBag}. - * - * @param bag the bag to merge - */ - public void merge(PriorityBag bag) { - bag.contents.forEach((priority, value) -> addAll(value, priority)); - this.noPriorityList.addAll(bag.noPriorityList); - } - - /** - * Add elements to the bag. - *

- * If the element's class is annotated with the {@link jakarta.annotation.Priority} - * annotation then that value will be used to determine priority otherwise the - * default priority value will be used. - * - * @param values the elements to add - */ - public void addAll(Iterable values) { - for (T value : values) { - add(value); - } - } - - /** - * Add elements to the bag. - * - * @param values the elements to add - * @param priority the priority to assign to the elements - */ - public void addAll(Iterable values, int priority) { - for (T value : values) { - add(value, priority); - } - } - - /** - * Add an element to the bag. - *

- * If the element's class is annotated with the {@link jakarta.annotation.Priority} - * annotation then that value will be used to determine priority otherwise the - * default priority value will be used. - * - * @param value the element to add - */ - public void add(T value) { - if (value != null) { - int priority; - // if (value instanceof Prioritized) { - // priority = ((Prioritized) value).priority(); - // } else { - Priority annotation = value.getClass().getAnnotation(Priority.class); - priority = annotation == null ? defaultPriority : annotation.value(); - // } - add(value, priority); - } - } - - /** - * Add an element to the bag with a specific priority. - *

- * - * @param value the element to add - * @param priority the priority of the element - */ - public void add(T value, int priority) { - if (value != null) { - if (priority < 0) { - noPriorityList.add(value); - } else { - contents.compute(priority, (key, list) -> combine(list, value)); - } - } - } - - /** - * Obtain the contents of this {@link PriorityBag} as - * an ordered {@link Stream}. - * - * @return the contents of this {@link PriorityBag} as - * an ordered {@link Stream} - */ - public Stream stream() { - Stream stream = contents.entrySet() - .stream() - .flatMap(e -> e.getValue().stream()); - - return Stream.concat(stream, noPriorityList.stream()); - } - - @Override - public Iterator iterator() { - return stream().iterator(); - } - - private List combine(List list, T value) { - if (list == null) { - list = new ArrayList<>(); - } - list.add(value); - return list; - } -} diff --git a/grpc/core/src/main/java/module-info.java b/grpc/core/src/main/java/module-info.java index 656c779f2ce..6fe54b37889 100644 --- a/grpc/core/src/main/java/module-info.java +++ b/grpc/core/src/main/java/module-info.java @@ -27,10 +27,7 @@ requires transitive io.grpc.protobuf; requires transitive io.grpc.protobuf.lite; - requires java.logging; - requires jakarta.inject; requires jakarta.annotation; exports io.helidon.grpc.core; - } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 9ae40f7e56d..0b429cf7406 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -20,7 +20,6 @@ import java.time.Duration; import java.util.Collections; import java.util.concurrent.Executor; -import java.util.logging.Logger; import io.helidon.common.buffers.BufferData; import io.helidon.http.Header; @@ -47,11 +46,13 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import static java.lang.System.Logger.Level.DEBUG; + /** * Base class for gRPC client calls. */ abstract class GrpcBaseClientCall extends ClientCall { - private static final Logger LOGGER = Logger.getLogger(GrpcBaseClientCall.class.getName()); + private static final System.Logger LOGGER = System.getLogger(GrpcBaseClientCall.class.getName()); protected static final Metadata EMPTY_METADATA = new Metadata(); protected static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); @@ -83,25 +84,25 @@ abstract class GrpcBaseClientCall extends ClientCall { this.responseMarshaller = methodDescriptor.getResponseMarshaller(); } - public Http2ClientConnection connection() { + protected Http2ClientConnection connection() { return connection; } - public MethodDescriptor.Marshaller requestMarshaller() { + protected MethodDescriptor.Marshaller requestMarshaller() { return requestMarshaller; } - public GrpcClientStream clientStream() { + protected GrpcClientStream clientStream() { return clientStream; } - public Listener responseListener() { + protected Listener responseListener() { return responseListener; } @Override public void start(Listener responseListener, Metadata metadata) { - LOGGER.finest("start called"); + LOGGER.log(DEBUG, "start called"); this.responseListener = responseListener; diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index d6fa87781f6..0cee556137d 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -22,7 +22,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Logger; import io.helidon.common.buffers.BufferData; import io.helidon.http.http2.Http2FrameData; @@ -32,6 +31,9 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.ERROR; + /** * * An implementation of a gRPC call. Expects: *

@@ -41,7 +43,7 @@ * @param response type */ class GrpcClientCall extends GrpcBaseClientCall { - private static final Logger LOGGER = Logger.getLogger(GrpcClientCall.class.getName()); + private static final System.Logger LOGGER = System.getLogger(GrpcClientCall.class.getName()); private final ExecutorService executor; private final AtomicInteger messageRequest = new AtomicInteger(); @@ -62,14 +64,14 @@ class GrpcClientCall extends GrpcBaseClientCall { @Override public void request(int numMessages) { - LOGGER.finest(() -> "request called " + numMessages); + LOGGER.log(DEBUG, () -> "request called " + numMessages); messageRequest.addAndGet(numMessages); startReadBarrier.countDown(); } @Override public void cancel(String message, Throwable cause) { - LOGGER.finest(() -> "cancel called " + message); + LOGGER.log(DEBUG, () -> "cancel called " + message); responseListener().onClose(Status.CANCELLED, EMPTY_METADATA); readStreamFuture.cancel(true); writeStreamFuture.cancel(true); @@ -78,13 +80,13 @@ public void cancel(String message, Throwable cause) { @Override public void halfClose() { - LOGGER.finest("halfClose called"); + LOGGER.log(DEBUG, "halfClose called"); sendingQueue.add(EMPTY_BUFFER_DATA); // end marker } @Override public void sendMessage(ReqT message) { - LOGGER.finest("sendMessage called"); + LOGGER.log(DEBUG, "sendMessage called"); BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); messageData.readFrom(requestMarshaller().stream(message)); BufferData headerData = BufferData.create(5); @@ -99,38 +101,38 @@ protected void startStreamingThreads() { writeStreamFuture = executor.submit(() -> { try { startWriteBarrier.await(); - LOGGER.fine("[Writing thread] started"); + LOGGER.log(DEBUG, "[Writing thread] started"); boolean endOfStream = false; while (isRemoteOpen()) { - LOGGER.finest("[Writing thread] polling sending queue"); + LOGGER.log(DEBUG, "[Writing thread] polling sending queue"); BufferData bufferData = sendingQueue.poll(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); if (bufferData != null) { if (bufferData == EMPTY_BUFFER_DATA) { // end marker - LOGGER.finest("[Writing thread] sending queue end marker found"); + LOGGER.log(DEBUG, "[Writing thread] sending queue end marker found"); if (!endOfStream) { - LOGGER.finest("[Writing thread] sending empty buffer to end stream"); + LOGGER.log(DEBUG, "[Writing thread] sending empty buffer to end stream"); clientStream().writeData(EMPTY_BUFFER_DATA, true); } break; } endOfStream = (sendingQueue.peek() == EMPTY_BUFFER_DATA); boolean lastEndOfStream = endOfStream; - LOGGER.finest(() -> "[Writing thread] writing bufferData " + lastEndOfStream); + LOGGER.log(DEBUG, () -> "[Writing thread] writing bufferData " + lastEndOfStream); clientStream().writeData(bufferData, endOfStream); } } } catch (Throwable e) { - LOGGER.finest(e.getMessage()); + LOGGER.log(ERROR, e.getMessage()); } - LOGGER.fine("[Writing thread] exiting"); + LOGGER.log(DEBUG, "[Writing thread] exiting"); }); // read streaming thread readStreamFuture = executor.submit(() -> { try { startReadBarrier.await(); - LOGGER.fine("[Reading thread] started"); + LOGGER.log(DEBUG, "[Reading thread] started"); // read response headers clientStream().readHeaders(); @@ -141,7 +143,7 @@ protected void startStreamingThreads() { // trailers received? if (clientStream().trailers().isDone()) { - LOGGER.finest("[Reading thread] trailers received"); + LOGGER.log(DEBUG, "[Reading thread] trailers received"); break; } @@ -150,29 +152,29 @@ protected void startStreamingThreads() { try { frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { - LOGGER.fine("[Reading thread] read timeout"); + LOGGER.log(DEBUG, "[Reading thread] read timeout"); continue; } if (frameData != null) { receivingQueue.add(frameData.data()); - LOGGER.finest("[Reading thread] adding bufferData to receiving queue"); + LOGGER.log(DEBUG, "[Reading thread] adding bufferData to receiving queue"); } } - LOGGER.finest("[Reading thread] closing listener"); + LOGGER.log(DEBUG, "[Reading thread] closing listener"); responseListener().onClose(Status.OK, EMPTY_METADATA); } catch (Throwable e) { - LOGGER.finest(e.getMessage()); + LOGGER.log(ERROR, e.getMessage()); responseListener().onClose(Status.UNKNOWN, EMPTY_METADATA); } finally { close(); } - LOGGER.fine("[Reading thread] exiting"); + LOGGER.log(DEBUG, "[Reading thread] exiting"); }); } private void close() { - LOGGER.finest("closing client call"); + LOGGER.log(DEBUG, "closing client call"); sendingQueue.clear(); clientStream().cancel(); connection().close(); @@ -180,11 +182,11 @@ private void close() { } private void drainReceivingQueue() { - LOGGER.finest("[Reading thread] draining receiving queue"); + LOGGER.log(DEBUG, "[Reading thread] draining receiving queue"); while (messageRequest.get() > 0 && !receivingQueue.isEmpty()) { messageRequest.getAndDecrement(); ResT res = toResponse(receivingQueue.remove()); - LOGGER.finest("[Reading thread] sending response to listener"); + LOGGER.log(DEBUG, "[Reading thread] sending response to listener"); responseListener().onMessage(res); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index c5c43470d8b..1688a60aa43 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -16,17 +16,12 @@ package io.helidon.webclient.grpc; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.Objects; -import io.helidon.grpc.core.InterceptorPriorities; import io.helidon.grpc.core.MarshallerSupplier; import io.helidon.grpc.core.MethodHandler; -import io.helidon.grpc.core.PriorityBag; import io.grpc.CallCredentials; -import io.grpc.ClientInterceptor; import io.grpc.MethodDescriptor; /** @@ -53,11 +48,6 @@ public final class GrpcClientMethodDescriptor { */ private final MethodDescriptor descriptor; - /** - * The list of client interceptors for this method. - */ - private final List interceptors; - /** * The {@link io.grpc.CallCredentials} for this method. */ @@ -70,12 +60,10 @@ public final class GrpcClientMethodDescriptor { private GrpcClientMethodDescriptor(String name, MethodDescriptor descriptor, - List interceptors, CallCredentials callCredentials, MethodHandler methodHandler) { this.name = name; this.descriptor = descriptor; - this.interceptors = interceptors; this.callCredentials = callCredentials; this.methodHandler = methodHandler; } @@ -214,15 +202,6 @@ public MethodDescriptor.MethodType type() { return descriptor.getType(); } - /** - * Obtain the {@link io.grpc.ClientInterceptor}s to use for this method. - * - * @return the {@link io.grpc.ClientInterceptor}s to use for this method - */ - List interceptors() { - return Collections.unmodifiableList(interceptors); - } - /** * Obtain the {@link MethodHandler} to use to make client calls. * @@ -255,27 +234,6 @@ public interface Rules { */ Rules responseType(Class type); - /** - * Register one or more {@link io.grpc.ClientInterceptor interceptors} for the method. - * - * @param interceptors the interceptor(s) to register - * @return this {@link GrpcClientMethodDescriptor.Rules} instance for - * fluent call chaining - */ - Rules intercept(ClientInterceptor... interceptors); - - /** - * Register one or more {@link io.grpc.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 io.grpc.ClientInterceptor}s to register - * @return this {@link GrpcClientMethodDescriptor.Rules} to allow - * fluent method chaining - */ - Rules intercept(int priority, ClientInterceptor... interceptors); - /** * Register the {@link MarshallerSupplier} for the method. *

@@ -318,7 +276,6 @@ public static class Builder private final MethodDescriptor.Builder descriptor; private Class requestType; private Class responseType; - private final PriorityBag interceptors = PriorityBag.withDefaultPriority(InterceptorPriorities.USER); private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); private MarshallerSupplier marshallerSupplier; private CallCredentials callCredentials; @@ -348,18 +305,6 @@ public Builder responseType(Class 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; @@ -367,11 +312,7 @@ public Builder marshallerSupplier(MarshallerSupplier supplier) { } Builder defaultMarshallerSupplier(MarshallerSupplier supplier) { - if (supplier == null) { - this.defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); - } else { - this.defaultMarshallerSupplier = supplier; - } + this.defaultMarshallerSupplier = Objects.requireNonNullElseGet(supplier, MarshallerSupplier::defaultInstance); return this; } @@ -423,7 +364,6 @@ public GrpcClientMethodDescriptor build() { return new GrpcClientMethodDescriptor(name, descriptor.build(), - interceptors.stream().toList(), callCredentials, methodHandler); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index ab5e13dcb6c..dcf7d0f5dc8 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -16,8 +16,6 @@ package io.helidon.webclient.grpc; -import java.util.logging.Logger; - import io.helidon.common.buffers.BufferData; import io.helidon.http.http2.Http2FrameData; import io.helidon.webclient.http2.StreamTimeoutException; @@ -26,6 +24,9 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.ERROR; + /** * An implementation of a unary gRPC call. Expects: *

@@ -35,7 +36,7 @@ * @param response type */ class GrpcUnaryClientCall extends GrpcBaseClientCall { - private static final Logger LOGGER = Logger.getLogger(GrpcUnaryClientCall.class.getName()); + private static final System.Logger LOGGER = System.getLogger(GrpcUnaryClientCall.class.getName()); private volatile boolean closeCalled; private volatile boolean requestSent; @@ -48,7 +49,7 @@ class GrpcUnaryClientCall extends GrpcBaseClientCall { @Override public void request(int numMessages) { - LOGGER.finest(() -> "request called " + numMessages); + LOGGER.log(DEBUG, () -> "request called " + numMessages); if (numMessages < 1) { close(Status.INVALID_ARGUMENT); } @@ -56,19 +57,19 @@ public void request(int numMessages) { @Override public void cancel(String message, Throwable cause) { - LOGGER.finest(() -> "cancel called " + message); + LOGGER.log(DEBUG, () -> "cancel called " + message); close(Status.CANCELLED); } @Override public void halfClose() { - LOGGER.finest("halfClose called"); + LOGGER.log(DEBUG, "halfClose called"); close(responseSent ? Status.OK : Status.UNKNOWN); } @Override public void sendMessage(ReqT message) { - LOGGER.finest("sendMessage called"); + LOGGER.log(DEBUG, "sendMessage called"); // should only be called once if (requestSent) { @@ -87,7 +88,7 @@ public void sendMessage(ReqT message) { while (isRemoteOpen()) { // trailers received? if (clientStream().trailers().isDone()) { - LOGGER.finest("trailers received"); + LOGGER.log(DEBUG, "trailers received"); return; } @@ -96,11 +97,11 @@ public void sendMessage(ReqT message) { try { frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { - LOGGER.fine("read timeout"); + LOGGER.log(ERROR, "read timeout"); continue; } if (frameData != null) { - LOGGER.finest("response received"); + LOGGER.log(DEBUG, "response received"); responseListener().onMessage(toResponse(frameData.data())); responseSent = true; } @@ -114,7 +115,7 @@ protected void startStreamingThreads() { private void close(Status status) { if (!closeCalled) { - LOGGER.finest("closing client call"); + LOGGER.log(ERROR, "closing client call"); responseListener().onClose(status, EMPTY_METADATA); clientStream().cancel(); connection().close(); diff --git a/webclient/grpc/src/main/java/module-info.java b/webclient/grpc/src/main/java/module-info.java index 26678febb9d..fc0984efc0c 100644 --- a/webclient/grpc/src/main/java/module-info.java +++ b/webclient/grpc/src/main/java/module-info.java @@ -32,12 +32,10 @@ requires transitive io.grpc; requires transitive io.grpc.stub; requires transitive io.helidon.builder.api; - requires transitive io.helidon.common.pki; requires transitive io.helidon.webclient.http2; requires transitive io.helidon.webclient; requires io.helidon.grpc.core; - requires java.logging; exports io.helidon.webclient.grpc; From 08170327aa930e624c291ac1aa3b319280e15a9f Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Thu, 14 Mar 2024 11:08:15 -0400 Subject: [PATCH 20/38] Switches to socket context logging. Throws more detailed exception if baseUri is not present. --- .../webclient/grpc/GrpcBaseClientCall.java | 9 +++- .../helidon/webclient/grpc/GrpcChannel.java | 4 +- .../webclient/grpc/GrpcClientCall.java | 44 +++++++++---------- .../GrpcClientProtocolConfigBlueprint.java | 3 +- .../webclient/grpc/GrpcUnaryClientCall.java | 16 +++---- 5 files changed, 42 insertions(+), 34 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 0b429cf7406..bdffd3cef10 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -22,6 +22,7 @@ import java.util.concurrent.Executor; import io.helidon.common.buffers.BufferData; +import io.helidon.common.socket.HelidonSocket; import io.helidon.http.Header; import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; @@ -75,6 +76,7 @@ abstract class GrpcBaseClientCall extends ClientCall { private volatile Http2ClientConnection connection; private volatile GrpcClientStream clientStream; private volatile Listener responseListener; + private volatile HelidonSocket socket; GrpcBaseClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { this.grpcClient = grpcClient; @@ -100,6 +102,10 @@ protected Listener responseListener() { return responseListener; } + protected HelidonSocket socket() { + return socket; + } + @Override public void start(Listener responseListener, Metadata metadata) { LOGGER.log(DEBUG, "start called"); @@ -108,6 +114,7 @@ public void start(Listener responseListener, Metadata metadata) { // obtain HTTP2 connection ClientConnection clientConnection = clientConnection(); + socket = clientConnection.helidonSocket(); connection = Http2ClientConnection.create((Http2ClientImpl) grpcClient.http2Client(), clientConnection, true); @@ -115,7 +122,7 @@ public void start(Listener responseListener, Metadata metadata) { clientStream = new GrpcClientStream( connection, Http2Settings.create(), // Http2Settings - clientConnection.helidonSocket(), // SocketContext + socket, // SocketContext new Http2StreamConfig() { @Override public boolean priorKnowledge() { 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 ee1ea90f3da..3efb29e47f7 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 @@ -50,7 +50,9 @@ public ClientCall newCall( @Override public String authority() { - ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); + ClientUri clientUri = grpcClient.prototype() + .baseUri() + .orElseThrow(() -> new IllegalArgumentException("No base URI provided for GrpcClient")); return clientUri.authority(); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 0cee556137d..2fc7224690a 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -35,7 +35,7 @@ import static java.lang.System.Logger.Level.ERROR; /** - * * An implementation of a gRPC call. Expects: + * An implementation of a gRPC call. Expects: *

* start (request | sendMessage)* (halfClose | cancel) * @@ -64,14 +64,14 @@ class GrpcClientCall extends GrpcBaseClientCall { @Override public void request(int numMessages) { - LOGGER.log(DEBUG, () -> "request called " + numMessages); + socket().log(LOGGER, DEBUG, "request called %d", numMessages); messageRequest.addAndGet(numMessages); startReadBarrier.countDown(); } @Override public void cancel(String message, Throwable cause) { - LOGGER.log(DEBUG, () -> "cancel called " + message); + socket().log(LOGGER, DEBUG, "cancel called %s", message); responseListener().onClose(Status.CANCELLED, EMPTY_METADATA); readStreamFuture.cancel(true); writeStreamFuture.cancel(true); @@ -80,13 +80,13 @@ public void cancel(String message, Throwable cause) { @Override public void halfClose() { - LOGGER.log(DEBUG, "halfClose called"); + socket().log(LOGGER, DEBUG, "halfClose called"); sendingQueue.add(EMPTY_BUFFER_DATA); // end marker } @Override public void sendMessage(ReqT message) { - LOGGER.log(DEBUG, "sendMessage called"); + socket().log(LOGGER, DEBUG, "sendMessage called"); BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); messageData.readFrom(requestMarshaller().stream(message)); BufferData headerData = BufferData.create(5); @@ -101,38 +101,38 @@ protected void startStreamingThreads() { writeStreamFuture = executor.submit(() -> { try { startWriteBarrier.await(); - LOGGER.log(DEBUG, "[Writing thread] started"); + socket().log(LOGGER, DEBUG, "[Writing thread] started"); boolean endOfStream = false; while (isRemoteOpen()) { - LOGGER.log(DEBUG, "[Writing thread] polling sending queue"); + socket().log(LOGGER, DEBUG, "[Writing thread] polling sending queue"); BufferData bufferData = sendingQueue.poll(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); if (bufferData != null) { if (bufferData == EMPTY_BUFFER_DATA) { // end marker - LOGGER.log(DEBUG, "[Writing thread] sending queue end marker found"); + socket().log(LOGGER, DEBUG, "[Writing thread] sending queue end marker found"); if (!endOfStream) { - LOGGER.log(DEBUG, "[Writing thread] sending empty buffer to end stream"); + socket().log(LOGGER, DEBUG, "[Writing thread] sending empty buffer to end stream"); clientStream().writeData(EMPTY_BUFFER_DATA, true); } break; } endOfStream = (sendingQueue.peek() == EMPTY_BUFFER_DATA); boolean lastEndOfStream = endOfStream; - LOGGER.log(DEBUG, () -> "[Writing thread] writing bufferData " + lastEndOfStream); + socket().log(LOGGER, DEBUG, "[Writing thread] writing bufferData %b", lastEndOfStream); clientStream().writeData(bufferData, endOfStream); } } } catch (Throwable e) { - LOGGER.log(ERROR, e.getMessage()); + socket().log(LOGGER, ERROR, e.getMessage(), e); } - LOGGER.log(DEBUG, "[Writing thread] exiting"); + socket().log(LOGGER, DEBUG, "[Writing thread] exiting"); }); // read streaming thread readStreamFuture = executor.submit(() -> { try { startReadBarrier.await(); - LOGGER.log(DEBUG, "[Reading thread] started"); + socket().log(LOGGER, DEBUG, "[Reading thread] started"); // read response headers clientStream().readHeaders(); @@ -143,7 +143,7 @@ protected void startStreamingThreads() { // trailers received? if (clientStream().trailers().isDone()) { - LOGGER.log(DEBUG, "[Reading thread] trailers received"); + socket().log(LOGGER, DEBUG, "[Reading thread] trailers received"); break; } @@ -152,29 +152,29 @@ protected void startStreamingThreads() { try { frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { - LOGGER.log(DEBUG, "[Reading thread] read timeout"); + socket().log(LOGGER, ERROR, "[Reading thread] read timeout"); continue; } if (frameData != null) { receivingQueue.add(frameData.data()); - LOGGER.log(DEBUG, "[Reading thread] adding bufferData to receiving queue"); + socket().log(LOGGER, DEBUG, "[Reading thread] adding bufferData to receiving queue"); } } - LOGGER.log(DEBUG, "[Reading thread] closing listener"); + socket().log(LOGGER, DEBUG, "[Reading thread] closing listener"); responseListener().onClose(Status.OK, EMPTY_METADATA); } catch (Throwable e) { - LOGGER.log(ERROR, e.getMessage()); + socket().log(LOGGER, ERROR, e.getMessage(), e); responseListener().onClose(Status.UNKNOWN, EMPTY_METADATA); } finally { close(); } - LOGGER.log(DEBUG, "[Reading thread] exiting"); + socket().log(LOGGER, DEBUG, "[Reading thread] exiting"); }); } private void close() { - LOGGER.log(DEBUG, "closing client call"); + socket().log(LOGGER, DEBUG, "closing client call"); sendingQueue.clear(); clientStream().cancel(); connection().close(); @@ -182,11 +182,11 @@ private void close() { } private void drainReceivingQueue() { - LOGGER.log(DEBUG, "[Reading thread] draining receiving queue"); + socket().log(LOGGER, DEBUG, "[Reading thread] draining receiving queue"); while (messageRequest.get() > 0 && !receivingQueue.isEmpty()) { messageRequest.getAndDecrement(); ResT res = toResponse(receivingQueue.remove()); - LOGGER.log(DEBUG, "[Reading thread] sending response to listener"); + socket().log(LOGGER, DEBUG, "[Reading thread] sending response to listener"); responseListener().onMessage(res); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java index 938277994e3..a88606091ad 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java @@ -21,7 +21,7 @@ import io.helidon.webclient.spi.ProtocolConfig; /** - * Configuration of an HTTP/1.1 client. + * Configuration of a gRPC client. */ @Prototype.Blueprint @Prototype.Configured @@ -35,5 +35,4 @@ default String type() { @Option.Default(GrpcProtocolProvider.CONFIG_KEY) @Override String name(); - } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index dcf7d0f5dc8..0c6e8017326 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -49,7 +49,7 @@ class GrpcUnaryClientCall extends GrpcBaseClientCall { @Override public void request(int numMessages) { - LOGGER.log(DEBUG, () -> "request called " + numMessages); + socket().log(LOGGER, DEBUG, "request called %d", numMessages); if (numMessages < 1) { close(Status.INVALID_ARGUMENT); } @@ -57,19 +57,19 @@ public void request(int numMessages) { @Override public void cancel(String message, Throwable cause) { - LOGGER.log(DEBUG, () -> "cancel called " + message); + socket().log(LOGGER, DEBUG, "cancel called %s", message); close(Status.CANCELLED); } @Override public void halfClose() { - LOGGER.log(DEBUG, "halfClose called"); + socket().log(LOGGER, DEBUG, "halfClose called"); close(responseSent ? Status.OK : Status.UNKNOWN); } @Override public void sendMessage(ReqT message) { - LOGGER.log(DEBUG, "sendMessage called"); + socket().log(LOGGER, DEBUG, "sendMessage called"); // should only be called once if (requestSent) { @@ -88,7 +88,7 @@ public void sendMessage(ReqT message) { while (isRemoteOpen()) { // trailers received? if (clientStream().trailers().isDone()) { - LOGGER.log(DEBUG, "trailers received"); + socket().log(LOGGER, DEBUG, "trailers received"); return; } @@ -97,11 +97,11 @@ public void sendMessage(ReqT message) { try { frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { - LOGGER.log(ERROR, "read timeout"); + socket().log(LOGGER, ERROR, "read timeout"); continue; } if (frameData != null) { - LOGGER.log(DEBUG, "response received"); + socket().log(LOGGER, DEBUG, "response received"); responseListener().onMessage(toResponse(frameData.data())); responseSent = true; } @@ -115,7 +115,7 @@ protected void startStreamingThreads() { private void close(Status status) { if (!closeCalled) { - LOGGER.log(ERROR, "closing client call"); + socket().log(LOGGER, DEBUG, "closing client call"); responseListener().onClose(status, EMPTY_METADATA); clientStream().cancel(); connection().close(); From ad662cd1398f5b1e38778693bbb1a6de2d69d9c6 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Thu, 14 Mar 2024 16:37:22 -0400 Subject: [PATCH 21/38] Converts PriorityBag to WeightedBag. Signed-off-by: Santiago Pericasgeertsen --- .../io/helidon/grpc/core/WeightedBag.java | 210 ++++++++++++++++++ .../io/helidon/grpc/core/WeightedBagTest.java | 148 ++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java create mode 100644 grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java new file mode 100644 index 00000000000..ae3f38c281b --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java @@ -0,0 +1,210 @@ +/* + * 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.grpc.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Stream; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +/** + * A bag of values ordered by weight. + *

+ * The higher the weight the higher the weight. For cases where weight is the same, + * elements are returned in the order that they were added to the bag. + *

+ * Elements added with negative weights are assumed to have no weight and + * will be least significant in order. + * + * @param the type of elements in the bag + * @see io.helidon.common.Weight + */ +public class WeightedBag implements Iterable { + + private final Map> contents; + private final List noWeightedList; + private final double defaultWeight; + + private WeightedBag(Map> contents, List noweightList, double defaultWeight) { + this.contents = contents; + this.noWeightedList = noweightList; + this.defaultWeight = defaultWeight; + } + + /** + * Create a new {@link WeightedBag} where elements added with no weight will be last + * in the order. + * + * @param the type of elements in the bag + * @return a new {@link WeightedBag} where elements + * dded with no weight will be last in the + * order + */ + public static WeightedBag create() { + return withDefaultWeight(-1.0); + } + + /** + * Create a new {@link WeightedBag} where elements added with no weight will be given + * a default weight value. + * + * @param weight the default weight value to assign + * to elements added with no weight + * @param the type of elements in the bag + * + * @return a new {@link WeightedBag} where elements + * added with no weight will be given + * a default weight value + */ + public static WeightedBag withDefaultWeight(double weight) { + return new WeightedBag<>(new TreeMap<>( + (o1, o2) -> Double.compare(o2, o1)), // reversed for weights + new ArrayList<>(), weight); + } + + /** + * Obtain a copy of this {@link WeightedBag}. + * + * @return a copy of this {@link WeightedBag} + */ + public WeightedBag copyMe() { + WeightedBag copy = WeightedBag.create(); + copy.merge(this); + return copy; + } + + /** + * Obtain an immutable copy of this {@link WeightedBag}. + * + * @return an immutable copy of this {@link WeightedBag} + */ + public WeightedBag readOnly() { + return new WeightedBag<>(Collections.unmodifiableMap(contents), + Collections.unmodifiableList(noWeightedList), + defaultWeight); + } + + /** + * Merge a {@link WeightedBag} into this {@link WeightedBag}. + * + * @param bag the bag to merge + */ + public void merge(WeightedBag bag) { + bag.contents.forEach((weight, value) -> addAll(value, weight)); + this.noWeightedList.addAll(bag.noWeightedList); + } + + /** + * Add elements to the bag. + *

+ * If the element's class is annotated with the {@link io.helidon.common.Weight} + * annotation then that value will be used to determine weight otherwise the + * default weight value will be used. + * + * @param values the elements to add + */ + public void addAll(Iterable values) { + for (T value : values) { + add(value); + } + } + + /** + * Add elements to the bag. + * + * @param values the elements to add + * @param weight the weight to assign to the elements + */ + public void addAll(Iterable values, double weight) { + for (T value : values) { + add(value, weight); + } + } + + /** + * Add an element to the bag. + *

+ * If the element's class is annotated with the {@link io.helidon.common.Weight} + * annotation then that value will be used to determine weight otherwise the + * default weight value will be used. + * + * @param value the element to add + */ + public void add(T value) { + if (value != null) { + double weight; + if (value instanceof Weighted weighted) { + weight = weighted.weight(); + } else { + Weight annotation = value.getClass().getAnnotation(Weight.class); + weight = annotation == null ? defaultWeight : annotation.value(); + } + add(value, weight); + } + } + + /** + * Add an element to the bag with a specific weight. + *

+ * + * @param value the element to add + * @param weight the weight of the element + */ + public void add(T value, double weight) { + if (value != null) { + if (weight < 0.0) { + noWeightedList.add(value); + } else { + contents.compute(weight, (key, list) -> combine(list, value)); + } + } + } + + /** + * Obtain the contents of this {@link WeightedBag} as + * an ordered {@link Stream}. + * + * @return the contents of this {@link WeightedBag} as + * an ordered {@link Stream} + */ + public Stream stream() { + Stream stream = contents.entrySet() + .stream() + .flatMap(e -> e.getValue().stream()); + + return Stream.concat(stream, noWeightedList.stream()); + } + + @Override + public Iterator iterator() { + return stream().iterator(); + } + + private List combine(List list, T value) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(value); + return list; + } +} diff --git a/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java b/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java new file mode 100644 index 00000000000..841952dc2e8 --- /dev/null +++ b/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java @@ -0,0 +1,148 @@ +/* + * 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.grpc.core; + +import java.util.Arrays; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; + +class WeightedBagTest { + + @Test + public void shouldReturnElementsInOrder() { + WeightedBag bag = WeightedBag.create(); + bag.add("Three", 3.0); + bag.add("Two", 2.0); + bag.add("One", 1.0); + assertThat(bag, contains("Three", "Two", "One")); + } + + @Test + public void shouldReturnElementsInOrderWithinSameWeight() { + WeightedBag bag = WeightedBag.create(); + bag.add("Two", 2.0); + bag.add("TwoToo", 2.0); + assertThat(bag, contains("Two", "TwoToo")); + } + + @Test + public void shouldReturnNoWeightElementsLast() { + WeightedBag bag = WeightedBag.create(); + bag.add("Three", 3.0); + bag.add("Last"); + bag.add("One", 1.0); + assertThat(bag, contains("Three", "One", "Last")); + } + + @Test + public void shouldGetWeightFromAnnotation() { + WeightedBag bag = WeightedBag.create(); + Value value = new Value(); + bag.add("One", 1.0); + bag.add("Three", 3.0); + bag.add(value); + assertThat(bag, contains("Three", value, "One")); + } + + @Test + public void shouldGetWeightFromWeighted() { + WeightedBag bag = WeightedBag.create(); + WeightedValue value = new WeightedValue(); + bag.add("One", 1.0); + bag.add("Three", 3.0); + bag.add(value); + assertThat(bag, contains("Three", value, "One")); + } + + @Test + public void shouldUseWeightFromWeightedOverAnnotation() { + WeightedBag bag = WeightedBag.create(); + AnnotatedWeightedValue value = new AnnotatedWeightedValue(); + bag.add("One", 1.0); + bag.add("Three", 3.0); + bag.add(value); + assertThat(bag, contains("Three", value, "One")); + } + + @Test + public void shouldUseDefaultWeight() { + WeightedBag bag = WeightedBag.withDefaultWeight(2); + bag.add("One", 1.0); + bag.add("Three", 3.0); + bag.add("Two"); + assertThat(bag, contains("Three", "Two", "One")); + } + + @Test + public void shouldAddAll() { + WeightedBag bag = WeightedBag.create(); + bag.addAll(Arrays.asList("Three", "Two", "One")); + assertThat(bag, contains("Three", "Two", "One")); + } + + @Test + public void shouldAddAllWithWeight() { + WeightedBag bag = WeightedBag.create(); + bag.add("First", 1.0); + bag.add("Last", 3.0); + bag.addAll(Arrays.asList("Three", "Two", "One"), 2.0); + assertThat(bag, contains("Last", "Three", "Two", "One", "First")); + } + + @Test + public void shouldMerge() { + WeightedBag bagOne = WeightedBag.create(); + WeightedBag bagTwo = WeightedBag.create(); + + bagOne.add("A", 1.0); + bagOne.add("B", 2.0); + bagOne.add("C", 2.0); + bagOne.add("D", 3.0); + + bagTwo.add("E", 1.0); + bagTwo.add("F", 3.0); + bagTwo.add("G", 3.0); + bagTwo.add("H", 4.0); + + bagOne.merge(bagTwo); + assertThat(bagOne, contains("H", "D", "F", "G", "B", "C", "A", "E")); + } + + @Weight(2.0) + public static class Value { + } + + public static class WeightedValue implements Weighted { + @Override + public double weight() { + return 2.0; + } + } + + @Weight(0.0) + public static class AnnotatedWeightedValue implements Weighted { + @Override + public double weight() { + return 2.0; + } + } +} From 46658701549665a2bc9df6479220da91bff1f07e Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Mon, 18 Mar 2024 16:18:07 -0400 Subject: [PATCH 22/38] Introduces support for client interceptors in low-level, stub-free API. --- .../helidon/grpc/core/InterceptorWeights.java | 63 ++++++++++ .../io/helidon/grpc/core/WeightedBag.java | 9 ++ .../grpc/GrpcClientMethodDescriptor.java | 53 ++++++++ .../webclient/grpc/GrpcServiceClientImpl.java | 54 ++++++-- .../grpc/tests/GrpcInterceptorTest.java | 119 ++++++++++++++++++ 5 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java create mode 100644 webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java new file mode 100644 index 00000000000..2975789be85 --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/InterceptorWeights.java @@ -0,0 +1,63 @@ +/* + * 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.grpc.core; + +/** + * gRPC interceptor weight classes. Higher weight means higher priority. + */ +public class InterceptorWeights { + + /** + * Context weight. + *

+ * Interceptors with this weight typically only perform tasks + * such as adding state to the call {@link io.grpc.Context}. + */ + public static final int CONTEXT = 5000; + + /** + * Tracing weight. + *

+ * Tracing and metrics interceptors are typically applied after any context + * interceptors so that they can trace and gather metrics on the whole call + * stack of remaining interceptors. + */ + public static final int TRACING = CONTEXT + 1; + + /** + * Security authentication weight. + */ + public static final int AUTHENTICATION = 2000; + + /** + * Security authorization weight. + */ + public static final int AUTHORIZATION = 2000; + + /** + * User-level weight. + *

+ * This value is also used as a default weight for application-supplied interceptors. + */ + public static final int USER = 1000; + + /** + * Cannot create instances. + */ + private InterceptorWeights() { + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java index ae3f38c281b..67c66742d85 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java @@ -82,6 +82,15 @@ public static WeightedBag withDefaultWeight(double weight) { new ArrayList<>(), weight); } + /** + * Check if bag is empty. + * + * @return outcome of test + */ + public boolean isEmpty() { + return contents.isEmpty() && noWeightedList.isEmpty(); + } + /** * Obtain a copy of this {@link WeightedBag}. * diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index 1688a60aa43..b28170216cc 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -16,12 +16,16 @@ package io.helidon.webclient.grpc; +import java.util.Arrays; import java.util.Objects; +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; /** @@ -48,6 +52,11 @@ public final class GrpcClientMethodDescriptor { */ private final MethodDescriptor descriptor; + /** + * The list of client interceptors for this method. + */ + private WeightedBag interceptors; + /** * The {@link io.grpc.CallCredentials} for this method. */ @@ -60,10 +69,12 @@ public final class GrpcClientMethodDescriptor { private GrpcClientMethodDescriptor(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; } @@ -146,6 +157,15 @@ public static Builder bidirectional(String serviceName, String name) { return builder(serviceName, name, MethodDescriptor.MethodType.BIDI_STREAMING); } + /** + * 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(); + } + /** * Return the {@link io.grpc.CallCredentials} set on this service. * @@ -234,6 +254,25 @@ public interface Rules { */ 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 weight the weight 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(double weight, ClientInterceptor... interceptors); + /** * Register the {@link MarshallerSupplier} for the method. *

@@ -276,6 +315,7 @@ public static class Builder private final MethodDescriptor.Builder descriptor; private Class requestType; private Class responseType; + private final WeightedBag interceptors = WeightedBag.withDefaultWeight(InterceptorWeights.USER); private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); private MarshallerSupplier marshallerSupplier; private CallCredentials callCredentials; @@ -305,6 +345,18 @@ public Builder responseType(Class type) { return this; } + @Override + public Builder intercept(ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + return this; + } + + @Override + public Builder intercept(double weight, ClientInterceptor... interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors), weight); + return this; + } + @Override public Builder marshallerSupplier(MarshallerSupplier supplier) { this.marshallerSupplier = supplier; @@ -364,6 +416,7 @@ public GrpcClientMethodDescriptor build() { return new GrpcClientMethodDescriptor(name, descriptor.build(), + interceptors, callCredentials, methodHandler); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java index 3b31b7d0200..e9faafaa899 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClientImpl.java @@ -19,26 +19,49 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +import io.helidon.grpc.core.WeightedBag; import io.grpc.CallOptions; +import io.grpc.Channel; import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; import io.grpc.MethodDescriptor; import io.grpc.stub.ClientCalls; import io.grpc.stub.StreamObserver; class GrpcServiceClientImpl implements GrpcServiceClient { - private final GrpcServiceDescriptor descriptor; + private final GrpcServiceDescriptor serviceDescriptor; + private final Channel serviceChannel; private final GrpcClientImpl grpcClient; + private final Map methodCache = new ConcurrentHashMap<>(); GrpcServiceClientImpl(GrpcServiceDescriptor descriptor, GrpcClientImpl grpcClient) { - this.descriptor = descriptor; + this.serviceDescriptor = descriptor; this.grpcClient = grpcClient; + + if (descriptor.interceptors().isEmpty()) { + serviceChannel = grpcClient.channel(); + } else { + // sort interceptors using a weighted bag + WeightedBag interceptors = WeightedBag.create(); + for (ClientInterceptor interceptor : descriptor.interceptors()) { + interceptors.add(interceptor); + } + + // wrap channel to call interceptors -- reversed for composition + List orderedInterceptors = interceptors.stream().toList().reversed(); + serviceChannel = ClientInterceptors.intercept(grpcClient.channel(), orderedInterceptors); + } } @Override public String serviceName() { - return descriptor.serviceName(); + return serviceDescriptor.serviceName(); } @Override @@ -152,13 +175,26 @@ public StreamObserver bidi(String methodName, StreamObserver< } private ClientCall ensureMethod(String methodName, MethodDescriptor.MethodType methodType) { - GrpcClientMethodDescriptor method = descriptor.method(methodName); - if (!method.type().equals(methodType)) { - throw new IllegalArgumentException("Method " + methodName + " is of type " + method.type() + GrpcClientMethodDescriptor methodDescriptor = serviceDescriptor.method(methodName); + if (!methodDescriptor.type().equals(methodType)) { + throw new IllegalArgumentException("Method " + methodName + " is of type " + methodDescriptor.type() + ", yet " + methodType + " was requested."); } - return methodType == MethodDescriptor.MethodType.UNARY - ? new GrpcUnaryClientCall<>(grpcClient, method.descriptor(), CallOptions.DEFAULT) - : new GrpcClientCall<>(grpcClient, method.descriptor(), CallOptions.DEFAULT); + + // use channel that contains all service and method interceptors + if (methodDescriptor.interceptors().isEmpty()) { + return serviceChannel.newCall(methodDescriptor.descriptor(), CallOptions.DEFAULT); + } else { + Channel methodChannel = methodCache.computeIfAbsent(methodName, k -> { + WeightedBag interceptors = WeightedBag.create(); + for (ClientInterceptor interceptor : serviceDescriptor.interceptors()) { + interceptors.add(interceptor); + } + interceptors.merge(methodDescriptor.interceptors()); + List orderedInterceptors = interceptors.stream().toList().reversed(); + return ClientInterceptors.intercept(grpcClient.channel(), orderedInterceptors); + }); + return methodChannel.newCall(methodDescriptor.descriptor(), CallOptions.DEFAULT); + } } } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java new file mode 100644 index 00000000000..692b405ce11 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java @@ -0,0 +1,119 @@ +/* + * 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.webclient.grpc.tests; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.helidon.common.Weight; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; +import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testing.junit5.ServerTest; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +/** + * Tests client interceptors using low-level API. + */ +@ServerTest +class GrpcInterceptorTest extends GrpcBaseTest { + + private final GrpcClient grpcClient; + private final GrpcServiceDescriptor serviceDescriptor; + private final List> calledInterceptors = new CopyOnWriteArrayList<>(); + + private GrpcInterceptorTest(WebServer server) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.grpcClient = GrpcClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + server.port()) + .build(); + this.serviceDescriptor = GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Upper", + GrpcClientMethodDescriptor.unary("StringService", "Upper") + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) + .intercept(new Weight50Interceptor()) + .intercept(new Weight500Interceptor()) + .build()) + .addInterceptor(new Weight100Interceptor()) + .addInterceptor(new Weight1000Interceptor()) + .addInterceptor(new Weight10Interceptor()) + .build(); + } + + @Test + void testUnaryUpper() { + Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) + .unary("Upper", newStringMessage("hello")); + assertThat(res.getText(), is("HELLO")); + assertThat(calledInterceptors, contains(Weight1000Interceptor.class, + Weight500Interceptor.class, + Weight100Interceptor.class, + Weight50Interceptor.class, + Weight10Interceptor.class)); + } + + class BaseInterceptor implements ClientInterceptor { + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, + Channel next) { + calledInterceptors.add(getClass()); + return next.newCall(method, callOptions); + } + } + + @Weight(10.0) + class Weight10Interceptor extends BaseInterceptor { + } + + @Weight(50.0) + class Weight50Interceptor extends BaseInterceptor { + } + + @Weight(100.0) + class Weight100Interceptor extends BaseInterceptor { + } + + @Weight(500.0) + class Weight500Interceptor extends BaseInterceptor { + } + + @Weight(1000.0) + class Weight1000Interceptor extends BaseInterceptor { + } +} From 92435ed6181606fd7d234b47d423b2a997cce777 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 19 Mar 2024 16:52:07 -0400 Subject: [PATCH 23/38] Basic support for client interceptors with service stubs --- .../io/helidon/webclient/grpc/GrpcClient.java | 20 ++++++ .../webclient/grpc/GrpcClientImpl.java | 7 ++ webclient/tests/grpc/pom.xml | 2 +- .../webclient/grpc/tests/GrpcBaseTest.java | 60 ++++++++++++++++- .../grpc/tests/GrpcInterceptorStubTest.java | 67 +++++++++++++++++++ .../grpc/tests/GrpcInterceptorTest.java | 42 +----------- .../webclient/grpc/tests/GrpcTest.java | 1 + 7 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorStubTest.java 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 c7b1e1795bc..6fb03c4d040 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 @@ -16,6 +16,7 @@ package io.helidon.webclient.grpc; +import java.util.Collection; import java.util.function.Consumer; import io.helidon.builder.api.RuntimeType; @@ -23,6 +24,7 @@ import io.helidon.webclient.spi.Protocol; import io.grpc.Channel; +import io.grpc.ClientInterceptor; /** * gRPC client. @@ -94,4 +96,22 @@ static GrpcClient create() { * @return a new gRPC channel */ Channel channel(); + + /** + * Create a gRPC channel for this client that can be used to create stubs. + * + * @param interceptors the array of client interceptors + * @return a new gRPC channel + */ + Channel channel(ClientInterceptor... interceptors); + + /** + * Create a gRPC channel for this client that can be used to create stubs. + * + * @param interceptors the list of client interceptors + * @return a new gRPC channel + */ + default Channel channel(Collection interceptors) { + return channel(interceptors.toArray(new ClientInterceptor[] {})); + } } 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 ec66b7cc28a..a30d01828ab 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 @@ -20,6 +20,8 @@ import io.helidon.webclient.http2.Http2Client; import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; class GrpcClientImpl implements GrpcClient { private final WebClient webClient; @@ -54,4 +56,9 @@ public GrpcServiceClient serviceClient(GrpcServiceDescriptor descriptor) { public Channel channel() { return new GrpcChannel(this); } + + @Override + public Channel channel(ClientInterceptor... interceptors) { + return ClientInterceptors.intercept(channel(), interceptors); + } } diff --git a/webclient/tests/grpc/pom.xml b/webclient/tests/grpc/pom.xml index 34d4c34cf32..a9fef9a5d7d 100644 --- a/webclient/tests/grpc/pom.xml +++ b/webclient/tests/grpc/pom.xml @@ -137,7 +137,7 @@ - com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier} + com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier} grpc-java io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java index 234d60a7d37..705876fbde9 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java @@ -20,17 +20,30 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.stub.StreamObserver; +import io.helidon.common.Weight; import io.helidon.common.configurable.Resource; import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.grpc.GrpcRouting; import io.helidon.webserver.testing.junit5.SetUpRoute; import io.helidon.webserver.testing.junit5.SetUpServer; - -import io.grpc.stub.StreamObserver; +import org.junit.jupiter.api.BeforeEach; class GrpcBaseTest { + private final List> calledInterceptors = new CopyOnWriteArrayList<>(); + + protected List> calledInterceptors() { + return calledInterceptors; + } + @SetUpServer public static void setup(WebServerConfig.Builder builder) { builder.tls(tls -> tls.privateKey(key -> key @@ -64,6 +77,11 @@ static void setUpRoute(GrpcRouting.Builder routing) { GrpcStubTest::echo); } + @BeforeEach + void setUpTest() { + calledInterceptors.clear(); + } + static void upper(Strings.StringMessage req, StreamObserver streamObserver) { Strings.StringMessage msg = Strings.StringMessage.newBuilder() @@ -178,4 +196,42 @@ public void onCompleted() { } }; } + + class BaseInterceptor implements ClientInterceptor { + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, + Channel next) { + calledInterceptors.add(getClass()); + return next.newCall(method, callOptions); + } + } + + @Weight(10.0) + class Weight10Interceptor extends BaseInterceptor { + } + + @Weight(50.0) + class Weight50Interceptor extends BaseInterceptor { + } + + @Weight(100.0) + class Weight100Interceptor extends BaseInterceptor { + } + + @Weight(500.0) + class Weight500Interceptor extends BaseInterceptor { + } + + @Weight(1000.0) + class Weight1000Interceptor extends BaseInterceptor { + } + + List allInterceptors() { + return List.of(new Weight10Interceptor(), + new Weight50Interceptor(), + new Weight100Interceptor(), + new Weight500Interceptor(), + new Weight1000Interceptor()); + } } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorStubTest.java new file mode 100644 index 00000000000..c602c488349 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorStubTest.java @@ -0,0 +1,67 @@ +/* + * 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.webclient.grpc.tests; + +import io.grpc.Channel; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testing.junit5.ServerTest; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +/** + * Tests gRPC client using stubs and TLS. + */ +@ServerTest +class GrpcInterceptorStubTest extends GrpcBaseTest { + + private final WebClient webClient; + + private GrpcInterceptorStubTest(WebServer server) { + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + this.webClient = WebClient.builder() + .tls(clientTls) + .baseUri("https://localhost:" + server.port()) + .build(); + } + + @Test + void testUnaryUpper() { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + Channel channel = grpcClient.channel(allInterceptors()); // channel with all interceptors + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(channel); + Strings.StringMessage res = service.upper(newStringMessage("hello")); + assertThat(res.getText(), is("HELLO")); + assertThat(calledInterceptors(), contains(Weight1000Interceptor.class, + Weight500Interceptor.class, + Weight100Interceptor.class, + Weight50Interceptor.class, + Weight10Interceptor.class)); + } +} diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java index 692b405ce11..6f279e5e8fd 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcInterceptorTest.java @@ -16,15 +16,6 @@ package io.helidon.webclient.grpc.tests; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.MethodDescriptor; -import io.helidon.common.Weight; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; import io.helidon.webclient.grpc.GrpcClient; @@ -46,7 +37,6 @@ class GrpcInterceptorTest extends GrpcBaseTest { private final GrpcClient grpcClient; private final GrpcServiceDescriptor serviceDescriptor; - private final List> calledInterceptors = new CopyOnWriteArrayList<>(); private GrpcInterceptorTest(WebServer server) { Tls clientTls = Tls.builder() @@ -80,40 +70,10 @@ void testUnaryUpper() { Strings.StringMessage res = grpcClient.serviceClient(serviceDescriptor) .unary("Upper", newStringMessage("hello")); assertThat(res.getText(), is("HELLO")); - assertThat(calledInterceptors, contains(Weight1000Interceptor.class, + assertThat(calledInterceptors(), contains(Weight1000Interceptor.class, Weight500Interceptor.class, Weight100Interceptor.class, Weight50Interceptor.class, Weight10Interceptor.class)); } - - class BaseInterceptor implements ClientInterceptor { - @Override - public ClientCall interceptCall(MethodDescriptor method, - CallOptions callOptions, - Channel next) { - calledInterceptors.add(getClass()); - return next.newCall(method, callOptions); - } - } - - @Weight(10.0) - class Weight10Interceptor extends BaseInterceptor { - } - - @Weight(50.0) - class Weight50Interceptor extends BaseInterceptor { - } - - @Weight(100.0) - class Weight100Interceptor extends BaseInterceptor { - } - - @Weight(500.0) - class Weight500Interceptor extends BaseInterceptor { - } - - @Weight(1000.0) - class Weight1000Interceptor extends BaseInterceptor { - } } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java index ef933137376..c3a7c67666b 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java @@ -47,6 +47,7 @@ class GrpcTest extends GrpcBaseTest { private static final long TIMEOUT_SECONDS = 10; private final GrpcClient grpcClient; + private final GrpcServiceDescriptor serviceDescriptor; private GrpcTest(WebServer server) { From 5e98f7b362e5c6452190e5caebae0ec744ea2c87 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 22 Apr 2024 17:01:34 -0400 Subject: [PATCH 24/38] Improves support for empty server streams. Includes fix in Http2ClientStream to allow transitions from HEADERS to END. New tests. --- .../io/helidon/webclient/grpc/GrpcClientCall.java | 2 +- .../helidon/webclient/grpc/tests/GrpcBaseTest.java | 13 ++++++++----- .../helidon/webclient/grpc/tests/GrpcStubTest.java | 8 ++++++++ .../io/helidon/webclient/grpc/tests/GrpcTest.java | 7 +++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 2fc7224690a..94a5fd185c0 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -153,7 +153,7 @@ protected void startStreamingThreads() { frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { socket().log(LOGGER, ERROR, "[Reading thread] read timeout"); - continue; + break; } if (frameData != null) { receivingQueue.add(frameData.data()); diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java index 705876fbde9..5f31f57e05f 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java @@ -93,12 +93,15 @@ static void upper(Strings.StringMessage req, static void split(Strings.StringMessage req, StreamObserver streamObserver) { - String[] strings = req.getText().split(" "); - for (String string : strings) { - streamObserver.onNext(Strings.StringMessage.newBuilder() - .setText(string) - .build()); + String reqString = req.getText(); + if (!reqString.isEmpty()) { + String[] strings = reqString.split(" "); + for (String s : strings) { + streamObserver.onNext(Strings.StringMessage.newBuilder() + .setText(s) + .build()); + } } streamObserver.onCompleted(); } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index 48b93578de4..bccc41a9209 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -86,6 +86,14 @@ void testServerStreamingSplit() { assertThat(res.hasNext(), is(false)); } + @Test + void testServerStreamingSplitEmpty() { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); + Iterator res = service.split(newStringMessage("")); + assertThat(res.hasNext(), is(false)); + } + @Test void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java index c3a7c67666b..6369c3102f4 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java @@ -116,6 +116,13 @@ void testServerStreamingSplit() { assertThat(res.hasNext(), is(false)); } + @Test + void testServerStreamingSplitEmpty() { + Iterator res = grpcClient.serviceClient(serviceDescriptor) + .serverStream("Split", newStringMessage("")); + assertThat(res.hasNext(), is(false)); + } + @Test void testServerStreamingSplitAsync() throws ExecutionException, InterruptedException, TimeoutException { CompletableFuture> future = new CompletableFuture<>(); From e6ff4a88b6a58c2d32d192d9aee9ef88e163a1aa Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Apr 2024 11:53:58 -0400 Subject: [PATCH 25/38] Adds more tests for client and server empty streams. --- .../webclient/grpc/GrpcClientCall.java | 1 + .../webclient/grpc/tests/GrpcStubTest.java | 11 +++++++++ .../webclient/grpc/tests/GrpcTest.java | 8 +++++++ .../src/test/resources/logging.properties | 24 +++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 webclient/tests/grpc/src/test/resources/logging.properties diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 94a5fd185c0..b4fb685975e 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -82,6 +82,7 @@ public void cancel(String message, Throwable cause) { public void halfClose() { socket().log(LOGGER, DEBUG, "halfClose called"); sendingQueue.add(EMPTY_BUFFER_DATA); // end marker + startWriteBarrier.countDown(); } @Override diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index bccc41a9209..ca0dc399ffc 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -133,4 +133,15 @@ void testBidirectionalEchoAsync() throws ExecutionException, InterruptedExceptio assertThat(res.next().getText(), is("world")); assertThat(res.hasNext(), is(false)); } + + @Test + void testBidirectionalEchoAsyncEmpty() throws ExecutionException, InterruptedException, TimeoutException { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient.channel()); + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver req = service.echo(multiStreamObserver(future)); + req.onCompleted(); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.hasNext(), is(false)); + } } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java index 6369c3102f4..bb7702225ab 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcTest.java @@ -16,6 +16,7 @@ package io.helidon.webclient.grpc.tests; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -166,6 +167,13 @@ void testBidirectionalEcho() { assertThat(res.hasNext(), is(false)); } + @Test + void testBidirectionalEchoEmpty() { + Iterator res = grpcClient.serviceClient(serviceDescriptor) + .bidi("Echo", Collections.emptyIterator()); + assertThat(res.hasNext(), is(false)); + } + @Test void testBidirectionalEchoAsync() throws ExecutionException, InterruptedException, TimeoutException { CompletableFuture> future = new CompletableFuture<>(); diff --git a/webclient/tests/grpc/src/test/resources/logging.properties b/webclient/tests/grpc/src/test/resources/logging.properties new file mode 100644 index 00000000000..344ba7085fd --- /dev/null +++ b/webclient/tests/grpc/src/test/resources/logging.properties @@ -0,0 +1,24 @@ +# +# 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. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +.level=INFO +#io.helidon.webclient.grpc.level=FINEST From 984b4ed9d43dc2529b44b9f9e67edeac53ae84bd Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 23 Apr 2024 14:49:32 -0400 Subject: [PATCH 26/38] Handles server EOS in HEADERS frame on the client side. Increased read timeout on client. --- .../io/helidon/webclient/grpc/GrpcBaseClientCall.java | 2 +- .../java/io/helidon/webclient/grpc/GrpcClientCall.java | 8 ++++---- .../io/helidon/webclient/http2/Http2ClientStream.java | 8 +++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index bdffd3cef10..06aa4d188e2 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -61,7 +61,7 @@ abstract class GrpcBaseClientCall extends ClientCall { protected static final int READ_TIMEOUT_SECONDS = 10; protected static final int BUFFER_SIZE_BYTES = 1024; - protected static final int WAIT_TIME_MILLIS = 100; + protected static final int WAIT_TIME_MILLIS = 2000; protected static final Duration WAIT_TIME_MILLIS_DURATION = Duration.ofMillis(WAIT_TIME_MILLIS); protected static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index b4fb685975e..0234c26f4e6 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -142,9 +142,9 @@ protected void startStreamingThreads() { // drain queue drainReceivingQueue(); - // trailers received? - if (clientStream().trailers().isDone()) { - socket().log(LOGGER, DEBUG, "[Reading thread] trailers received"); + // trailers or eos received? + if (clientStream().trailers().isDone() || !clientStream().hasEntity()) { + socket().log(LOGGER, DEBUG, "[Reading thread] trailers or eos received"); break; } @@ -154,7 +154,7 @@ protected void startStreamingThreads() { frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); } catch (StreamTimeoutException e) { socket().log(LOGGER, ERROR, "[Reading thread] read timeout"); - break; + continue; } if (frameData != null) { receivingQueue.add(frameData.data()); diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index 5321b846d81..ccf0b78db71 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -195,7 +195,13 @@ public CompletableFuture trailers() { return trailers; } - boolean hasEntity() { + /** + * Determines if an entity is expected. Set to {@code false} when an EOS flag + * is received. + * + * @return {@code true} if entity expected, {@code false} otherwise. + */ + public boolean hasEntity() { return hasEntity; } From 7becac278f575dc41b09c3d1c0052bfc7b07071a Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 24 Apr 2024 14:55:46 -0400 Subject: [PATCH 27/38] Some new tests from JK and fixes to gRPC client code to handle: (1) exceptions thrown in gRPC methods and (2) larger payloads. --- .../webclient/grpc/GrpcClientCall.java | 18 ++++++++-- .../webclient/grpc/GrpcUnaryClientCall.java | 11 +++--- .../tests/grpc/src/main/proto/strings.proto | 2 ++ .../webclient/grpc/tests/GrpcBaseTest.java | 11 +++++- .../webclient/grpc/tests/GrpcStubTest.java | 34 +++++++++++++++++++ .../src/test/resources/logging.properties | 2 +- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 0234c26f4e6..13549c464cd 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -16,6 +16,10 @@ package io.helidon.webclient.grpc; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -88,8 +92,18 @@ public void halfClose() { @Override public void sendMessage(ReqT message) { socket().log(LOGGER, DEBUG, "sendMessage called"); - BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); - messageData.readFrom(requestMarshaller().stream(message)); + + // serialize message using a marshaller + ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE_BYTES); + try (InputStream is = requestMarshaller().stream(message)) { + is.transferTo(baos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + byte[] serialized = baos.toByteArray(); + + // queue data message and start writer + BufferData messageData = BufferData.createReadOnly(serialized, 0, serialized.length); BufferData headerData = BufferData.create(5); headerData.writeInt8(0); // no compression headerData.writeUnsignedInt32(messageData.available()); // length prefixed diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index 0c6e8017326..e2c9a87fd81 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -85,11 +85,14 @@ public void sendMessage(ReqT message) { clientStream().writeData(BufferData.create(headerData, messageData), true); requestSent = true; + // read response headers + clientStream().readHeaders(); + while (isRemoteOpen()) { - // trailers received? - if (clientStream().trailers().isDone()) { - socket().log(LOGGER, DEBUG, "trailers received"); - return; + // trailers or eos received? + if (clientStream().trailers().isDone() || !clientStream().hasEntity()) { + socket().log(LOGGER, DEBUG, "[Reading thread] trailers or eos received"); + break; } // attempt to read and queue diff --git a/webclient/tests/grpc/src/main/proto/strings.proto b/webclient/tests/grpc/src/main/proto/strings.proto index 2da17d2f5de..f04386897b4 100644 --- a/webclient/tests/grpc/src/main/proto/strings.proto +++ b/webclient/tests/grpc/src/main/proto/strings.proto @@ -23,6 +23,8 @@ service StringService { rpc Split (StringMessage) returns (stream StringMessage) {} rpc Join (stream StringMessage) returns (StringMessage) {} rpc Echo (stream StringMessage) returns (stream StringMessage) {} + rpc BadMethod (StringMessage) returns (StringMessage) {} + rpc NotImplementedMethod (StringMessage) returns (StringMessage) {} } message StringMessage { diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java index 5f31f57e05f..82897279b5a 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcBaseTest.java @@ -27,6 +27,7 @@ import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.MethodDescriptor; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import io.helidon.common.Weight; import io.helidon.common.configurable.Resource; @@ -74,7 +75,11 @@ static void setUpRoute(GrpcRouting.Builder routing) { .bidi(Strings.getDescriptor(), "StringService", "Echo", - GrpcStubTest::echo); + GrpcStubTest::echo) + .unary(Strings.getDescriptor(), + "StringService", + "BadMethod", + GrpcStubTest::badMethod); } @BeforeEach @@ -154,6 +159,10 @@ public void onCompleted() { }; } + static void badMethod(Strings.StringMessage req, StreamObserver streamObserver) { + streamObserver.onError(Status.INTERNAL.asException()); + } + Strings.StringMessage newStringMessage(String data) { return Strings.StringMessage.newBuilder().setText(data).build(); } diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index ca0dc399ffc..935bb22aca7 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -16,11 +16,14 @@ package io.helidon.webclient.grpc.tests; +import java.nio.charset.StandardCharsets; import java.util.Iterator; +import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.IntStream; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; @@ -28,6 +31,7 @@ import io.helidon.webclient.grpc.GrpcClient; import io.helidon.webserver.WebServer; import io.helidon.webserver.testing.junit5.ServerTest; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.grpc.stub.StreamObserver; @@ -144,4 +148,34 @@ void testBidirectionalEchoAsyncEmpty() throws ExecutionException, InterruptedExc Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(res.hasNext(), is(false)); } + + @Test + void testBidirectionalEchoAsyncWithLargePayload() throws ExecutionException, InterruptedException, TimeoutException { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceStub service = StringServiceGrpc.newStub(grpcClient.channel()); + CompletableFuture> future = new CompletableFuture<>(); + StreamObserver req = service.echo(multiStreamObserver(future)); + byte[] array = new byte[2000]; + new Random().nextBytes(array); + String largeString = new String(array, StandardCharsets.UTF_8); + req.onNext(newStringMessage(largeString)); + req.onCompleted(); + Iterator res = future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertThat(res.next().getText(), is(largeString)); + assertThat(res.hasNext(), is(false)); + } + + @Test + void testReceiveServerException() { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); + Assertions.assertThrows(Throwable.class, () -> service.badMethod(newStringMessage("hello"))); + } + + @Test + void testCallingNotImplementMethodThrowsException() { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); + Assertions.assertThrows(Throwable.class, () -> service.notImplementedMethod(newStringMessage("hello"))); + } } diff --git a/webclient/tests/grpc/src/test/resources/logging.properties b/webclient/tests/grpc/src/test/resources/logging.properties index 344ba7085fd..09e74ff2922 100644 --- a/webclient/tests/grpc/src/test/resources/logging.properties +++ b/webclient/tests/grpc/src/test/resources/logging.properties @@ -21,4 +21,4 @@ handlers=io.helidon.logging.jul.HelidonConsoleHandler java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n .level=INFO -#io.helidon.webclient.grpc.level=FINEST +io.helidon.webclient.grpc.level=FINEST From ee05b3ab03643df6a9b9bfc0628d05fe4947cfb5 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 25 Apr 2024 10:04:02 -0400 Subject: [PATCH 28/38] Fixes problems in unary calls with large payloads. --- .../webclient/grpc/GrpcBaseClientCall.java | 13 +++++++++++++ .../helidon/webclient/grpc/GrpcClientCall.java | 16 ++-------------- .../webclient/grpc/GrpcUnaryClientCall.java | 5 +++-- .../webclient/grpc/tests/GrpcStubTest.java | 11 ++++++++++- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 06aa4d188e2..37b79065523 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -16,7 +16,10 @@ package io.helidon.webclient.grpc; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.time.Duration; import java.util.Collections; import java.util.concurrent.Executor; @@ -214,4 +217,14 @@ public int read() { } }); } + + protected byte[] serializeMessage(ReqT message) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE_BYTES); + try (InputStream is = requestMarshaller().stream(message)) { + is.transferTo(baos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return baos.toByteArray(); + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 13549c464cd..56a836e22b9 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -16,10 +16,6 @@ package io.helidon.webclient.grpc; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -93,16 +89,8 @@ public void halfClose() { public void sendMessage(ReqT message) { socket().log(LOGGER, DEBUG, "sendMessage called"); - // serialize message using a marshaller - ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE_BYTES); - try (InputStream is = requestMarshaller().stream(message)) { - is.transferTo(baos); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - byte[] serialized = baos.toByteArray(); - - // queue data message and start writer + // serialize and queue message for writing + byte[] serialized = serializeMessage(message); BufferData messageData = BufferData.createReadOnly(serialized, 0, serialized.length); BufferData headerData = BufferData.create(5); headerData.writeInt8(0); // no compression diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index e2c9a87fd81..ac3291bd248 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -77,8 +77,9 @@ public void sendMessage(ReqT message) { return; } - BufferData messageData = BufferData.growing(BUFFER_SIZE_BYTES); - messageData.readFrom(requestMarshaller().stream(message)); + // serialize and write message + byte[] serialized = serializeMessage(message); + BufferData messageData = BufferData.createReadOnly(serialized, 0, serialized.length); BufferData headerData = BufferData.create(5); headerData.writeInt8(0); // no compression headerData.writeUnsignedInt32(messageData.available()); // length prefixed diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java index 935bb22aca7..e1701dca81e 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcStubTest.java @@ -16,6 +16,7 @@ package io.helidon.webclient.grpc.tests; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Random; @@ -23,7 +24,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.stream.IntStream; import io.helidon.common.configurable.Resource; import io.helidon.common.tls.Tls; @@ -70,6 +70,15 @@ void testUnaryUpper() { assertThat(res.getText(), is("HELLO")); } + @Test + void tesUnaryUpperLongString() { + GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); + String s = CharBuffer.allocate(2000).toString().replace('\0', 'a'); + Strings.StringMessage res = service.upper(newStringMessage(s)); + assertThat(res.getText(), is(s.toUpperCase())); + } + @Test void testUnaryUpperAsync() throws ExecutionException, InterruptedException, TimeoutException { GrpcClient grpcClient = webClient.client(GrpcClient.PROTOCOL); From 309f8a47ca9676549180229084affcaa22788186 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 2 May 2024 09:39:36 -0400 Subject: [PATCH 29/38] Use protobuf version for protoc. Signed-off-by: Santiago Pericas-Geertsen --- webclient/tests/grpc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webclient/tests/grpc/pom.xml b/webclient/tests/grpc/pom.xml index a9fef9a5d7d..d3a41760e97 100644 --- a/webclient/tests/grpc/pom.xml +++ b/webclient/tests/grpc/pom.xml @@ -137,7 +137,7 @@ - com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier} + com.google.protobuf:protoc:${version.lib.google-protobuf}:exe:${os.detected.classifier} grpc-java io.grpc:protoc-gen-grpc-java:${version.lib.grpc}:exe:${os.detected.classifier} From a11da57da25da5352b4a69e20432a6e4b67251ff Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Fri, 3 May 2024 11:20:00 -0400 Subject: [PATCH 30/38] Basic support for gRPC protocol config settings --- webclient/grpc/pom.xml | 5 ++ .../webclient/grpc/GrpcBaseClientCall.java | 24 ++++++--- .../webclient/grpc/GrpcClientCall.java | 13 +++-- .../grpc/GrpcClientConfigBlueprint.java | 3 +- .../GrpcClientProtocolConfigBlueprint.java | 37 ++++++++++++++ .../webclient/grpc/GrpcUnaryClientCall.java | 10 +++- .../webclient/grpc/tests/GrpcConfigTest.java | 50 +++++++++++++++++++ .../grpc/src/test/resources/application.yaml | 20 ++++++++ 8 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java create mode 100644 webclient/tests/grpc/src/test/resources/application.yaml diff --git a/webclient/grpc/pom.xml b/webclient/grpc/pom.xml index d4630a4f5f6..e86fc5ed478 100644 --- a/webclient/grpc/pom.xml +++ b/webclient/grpc/pom.xml @@ -61,6 +61,11 @@ helidon-common-features-api true + + io.helidon.config + helidon-config-yaml + test + org.junit.jupiter junit-jupiter-api diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 37b79065523..a7f7ead82f8 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -62,16 +62,14 @@ abstract class GrpcBaseClientCall extends ClientCall { protected static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); protected static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); - protected static final int READ_TIMEOUT_SECONDS = 10; - protected static final int BUFFER_SIZE_BYTES = 1024; - protected static final int WAIT_TIME_MILLIS = 2000; - protected static final Duration WAIT_TIME_MILLIS_DURATION = Duration.ofMillis(WAIT_TIME_MILLIS); - protected static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); private final GrpcClientImpl grpcClient; private final MethodDescriptor methodDescriptor; private final CallOptions callOptions; + private final int initBufferSize; + private final Duration pollWaitTime; + private final boolean abortPollTimeExpired; private final MethodDescriptor.Marshaller requestMarshaller; private final MethodDescriptor.Marshaller responseMarshaller; @@ -87,6 +85,17 @@ abstract class GrpcBaseClientCall extends ClientCall { this.callOptions = callOptions; this.requestMarshaller = methodDescriptor.getRequestMarshaller(); this.responseMarshaller = methodDescriptor.getResponseMarshaller(); + this.initBufferSize = grpcClient.prototype().protocolConfig().initBufferSize(); + this.pollWaitTime = grpcClient.prototype().protocolConfig().pollWaitTime(); + this.abortPollTimeExpired = grpcClient.prototype().protocolConfig().abortPollTimeExpired(); + } + + protected boolean abortPollTimeExpired() { + return abortPollTimeExpired; + } + + protected Duration pollWaitTime() { + return pollWaitTime; } protected Http2ClientConnection connection() { @@ -139,7 +148,8 @@ public int priority() { @Override public Duration readTimeout() { - return grpcClient.prototype().readTimeout().orElse(Duration.ofSeconds(READ_TIMEOUT_SECONDS)); + return grpcClient.prototype().readTimeout().orElse( + grpcClient.prototype().protocolConfig().pollWaitTime()); } }, null, // Http2ClientConfig @@ -219,7 +229,7 @@ public int read() { } protected byte[] serializeMessage(ReqT message) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE_BYTES); + ByteArrayOutputStream baos = new ByteArrayOutputStream(initBufferSize); try (InputStream is = requestMarshaller().stream(message)) { is.transferTo(baos); } catch (IOException e) { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 56a836e22b9..a9bb3baae95 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -109,7 +109,7 @@ protected void startStreamingThreads() { boolean endOfStream = false; while (isRemoteOpen()) { socket().log(LOGGER, DEBUG, "[Writing thread] polling sending queue"); - BufferData bufferData = sendingQueue.poll(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS); + BufferData bufferData = sendingQueue.poll(pollWaitTime().toMillis(), TimeUnit.MILLISECONDS); if (bufferData != null) { if (bufferData == EMPTY_BUFFER_DATA) { // end marker socket().log(LOGGER, DEBUG, "[Writing thread] sending queue end marker found"); @@ -153,9 +153,14 @@ protected void startStreamingThreads() { // attempt to read and queue Http2FrameData frameData; try { - frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); + frameData = clientStream().readOne(pollWaitTime()); } catch (StreamTimeoutException e) { - socket().log(LOGGER, ERROR, "[Reading thread] read timeout"); + // abort or retry based on config settings + if (abortPollTimeExpired()) { + socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, aborting"); + throw e; // caught below + } + socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, retrying"); continue; } if (frameData != null) { @@ -166,6 +171,8 @@ protected void startStreamingThreads() { socket().log(LOGGER, DEBUG, "[Reading thread] closing listener"); responseListener().onClose(Status.OK, EMPTY_METADATA); + } catch (StreamTimeoutException e) { + responseListener().onClose(Status.DEADLINE_EXCEEDED, EMPTY_METADATA); } catch (Throwable e) { socket().log(LOGGER, ERROR, e.getMessage(), e); responseListener().onClose(Status.UNKNOWN, EMPTY_METADATA); diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java index 8d1821acfea..7403c76a0ad 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java @@ -26,8 +26,9 @@ @Prototype.Blueprint @Prototype.Configured interface GrpcClientConfigBlueprint extends HttpClientConfig, Prototype.Factory { + /** - * WebSocket specific configuration. + * gRPC specific configuration. * * @return protocol specific configuration */ diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java index a88606091ad..4c8feac79cc 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java @@ -16,8 +16,11 @@ package io.helidon.webclient.grpc; +import java.time.Duration; + import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; +import io.helidon.common.socket.SocketOptions; import io.helidon.webclient.spi.ProtocolConfig; /** @@ -26,6 +29,7 @@ @Prototype.Blueprint @Prototype.Configured interface GrpcClientProtocolConfigBlueprint extends ProtocolConfig { + @Override default String type() { return GrpcProtocolProvider.CONFIG_KEY; @@ -35,4 +39,37 @@ default String type() { @Option.Default(GrpcProtocolProvider.CONFIG_KEY) @Override String name(); + + /** + * How long to wait for the next HTTP/2 data frame to arrive in underlying stream. + * Whether this is a fatal error or not is controlled by {@link #abortPollTimeExpired()}. + * + * @return poll time as a duration + * @see SocketOptions#readTimeout() + */ + @Option.Configured + @Option.Default("PT10S") + Duration pollWaitTime(); + + /** + * Whether to continue retrying after a poll wait timeout expired or not. If a read + * operation timeouts out and this flag is set to {@code false}, the event is logged + * and the client will retry. Otherwise, an exception is thrown. + * + * @return abort timeout flag + */ + @Option.Configured + @Option.Default("false") + boolean abortPollTimeExpired(); + + /** + * Initial buffer size used to serialize gRPC request payloads. Buffers shall grow + * according to the payload size, but setting this initial buffer size to a larger value + * may improve performance for certain applications. + * + * @return initial buffer size + */ + @Option.Configured + @Option.Default("2048") + int initBufferSize(); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index ac3291bd248..d0cde6fdcdb 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -99,9 +99,15 @@ public void sendMessage(ReqT message) { // attempt to read and queue Http2FrameData frameData; try { - frameData = clientStream().readOne(WAIT_TIME_MILLIS_DURATION); + frameData = clientStream().readOne(pollWaitTime()); } catch (StreamTimeoutException e) { - socket().log(LOGGER, ERROR, "read timeout"); + // abort or retry based on config settings + if (abortPollTimeExpired()) { + socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, aborting"); + responseListener().onClose(Status.DEADLINE_EXCEEDED, EMPTY_METADATA); + break; + } + socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, retrying"); continue; } if (frameData != null) { diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java new file mode 100644 index 00000000000..dd26f4c4821 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java @@ -0,0 +1,50 @@ +/* + * 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.webclient.grpc.tests; + +import java.time.Duration; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.webclient.grpc.GrpcClientProtocolConfig; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Tests gRPC client config protocol settings. + */ +class GrpcConfigProtocolTest { + + @Test + void testDefaults() { + GrpcClientProtocolConfig config = GrpcClientProtocolConfig.create(); + assertThat(config.pollWaitTime(), is(Duration.ofSeconds(10))); + assertThat(config.abortPollTimeExpired(), is(false)); + assertThat(config.initBufferSize(), is(2048)); + } + + @Test + void testApplicationConfig() { + GrpcClientProtocolConfig config = GrpcClientProtocolConfig.create( + Config.create(ConfigSources.classpath("application.yaml")).get("grpc-client")); + assertThat(config.pollWaitTime(), is(Duration.ofSeconds(30))); + assertThat(config.abortPollTimeExpired(), is(true)); + assertThat(config.initBufferSize(), is(10000)); + } +} diff --git a/webclient/tests/grpc/src/test/resources/application.yaml b/webclient/tests/grpc/src/test/resources/application.yaml new file mode 100644 index 00000000000..fd5df3d4369 --- /dev/null +++ b/webclient/tests/grpc/src/test/resources/application.yaml @@ -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. +# + +grpc-client: + poll-wait-time: PT30S + abort-poll-time-expired: true + init-buffer-size: 10000 \ No newline at end of file From 308161d703cb720bcbc64230c107c3f625edaa18 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 6 May 2024 09:32:21 -0400 Subject: [PATCH 31/38] Javadoc updates. Signed-off-by: Santiago Pericas-Geertsen --- .../io/helidon/grpc/core/WeightedBag.java | 1 - .../GrpcClientProtocolConfigBlueprint.java | 13 +++++++-- .../grpc/GrpcServiceDescriptorBlueprint.java | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java index 67c66742d85..202cc20c92f 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java @@ -174,7 +174,6 @@ public void add(T value) { /** * Add an element to the bag with a specific weight. - *

* * @param value the element to add * @param weight the weight of the element diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java index 4c8feac79cc..afa3aa870ae 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java @@ -20,7 +20,6 @@ import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; -import io.helidon.common.socket.SocketOptions; import io.helidon.webclient.spi.ProtocolConfig; /** @@ -30,11 +29,21 @@ @Prototype.Configured interface GrpcClientProtocolConfigBlueprint extends ProtocolConfig { + /** + * Type identifying this protocol. + * + * @return protocol type + */ @Override default String type() { return GrpcProtocolProvider.CONFIG_KEY; } + /** + * Name identifying this client protocol. Defaults to type. + * + * @return name of client protocol + */ @Option.Configured @Option.Default(GrpcProtocolProvider.CONFIG_KEY) @Override @@ -45,7 +54,7 @@ default String type() { * Whether this is a fatal error or not is controlled by {@link #abortPollTimeExpired()}. * * @return poll time as a duration - * @see SocketOptions#readTimeout() + * @see io.helidon.common.socket.SocketOptions#readTimeout() */ @Option.Configured @Option.Default("PT10S") diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java index 0693528e6fc..6018915b50e 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceDescriptorBlueprint.java @@ -29,11 +29,28 @@ @Prototype.Blueprint interface GrpcServiceDescriptorBlueprint { + /** + * Service name. + * + * @return the server name + */ String serviceName(); + /** + * Map of names to gRPC method descriptors. + * + * @return method map + */ @Option.Singular Map methods(); + /** + * Descriptor for a given method. + * + * @param name method name + * @return method descriptor + * @throws NoSuchElementException if not found + */ default GrpcClientMethodDescriptor method(String name) { GrpcClientMethodDescriptor descriptor = methods().get(name); if (descriptor == null) { @@ -42,8 +59,18 @@ default GrpcClientMethodDescriptor method(String name) { return descriptor; } + /** + * Ordered list of method interceptors. + * + * @return list of interceptors + */ @Option.Singular List interceptors(); + /** + * Credentials for this call, if any. + * + * @return optional credentials + */ Optional callCredentials(); } From e17d6ee5bfda7ea7363c7813e391274429ba554c Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 20 May 2024 13:19:52 -0400 Subject: [PATCH 32/38] Improves handling for server connection drops. On a read timeout, if the abort flag is not set, attempts to send a PING frame to verify the connection's health. --- .../webclient/grpc/GrpcClientCall.java | 44 +++++++-- .../GrpcClientProtocolConfigBlueprint.java | 3 + .../webclient/http2/Http2ClientStream.java | 10 ++ .../grpc/tests/GrpcConnectionErrorTest.java | 95 +++++++++++++++++++ 4 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index a9bb3baae95..70c51e1645c 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -127,6 +127,8 @@ protected void startStreamingThreads() { } } catch (Throwable e) { socket().log(LOGGER, ERROR, e.getMessage(), e); + Status errorStatus = Status.UNKNOWN.withDescription(e.getMessage()).withCause(e); + responseListener().onClose(errorStatus, EMPTY_METADATA); } socket().log(LOGGER, DEBUG, "[Writing thread] exiting"); }); @@ -138,8 +140,17 @@ protected void startStreamingThreads() { socket().log(LOGGER, DEBUG, "[Reading thread] started"); // read response headers - clientStream().readHeaders(); + boolean headersRead = false; + do { + try { + clientStream().readHeaders(); + headersRead = true; + } catch (StreamTimeoutException e) { + handleStreamTimeout(e); + } + } while (!headersRead); + // read data from stream while (isRemoteOpen()) { // drain queue drainReceivingQueue(); @@ -155,12 +166,7 @@ protected void startStreamingThreads() { try { frameData = clientStream().readOne(pollWaitTime()); } catch (StreamTimeoutException e) { - // abort or retry based on config settings - if (abortPollTimeExpired()) { - socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, aborting"); - throw e; // caught below - } - socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, retrying"); + handleStreamTimeout(e); continue; } if (frameData != null) { @@ -175,7 +181,8 @@ protected void startStreamingThreads() { responseListener().onClose(Status.DEADLINE_EXCEEDED, EMPTY_METADATA); } catch (Throwable e) { socket().log(LOGGER, ERROR, e.getMessage(), e); - responseListener().onClose(Status.UNKNOWN, EMPTY_METADATA); + Status errorStatus = Status.UNKNOWN.withDescription(e.getMessage()).withCause(e); + responseListener().onClose(errorStatus, EMPTY_METADATA); } finally { close(); } @@ -200,4 +207,25 @@ private void drainReceivingQueue() { responseListener().onMessage(res); } } + + /** + * Handles a read timeout by either aborting or continuing, depending on how the client + * is configured. If not aborting, it will attempt to send a PING frame to check the + * connection health before attempting to proceed. + * + * @param e a stream timeout exception + */ + private void handleStreamTimeout(StreamTimeoutException e) { + // abort or retry based on config settings + if (abortPollTimeExpired()) { + socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, aborting"); + throw e; // caught below + } + + // check connection health before proceeding + clientStream().sendPing(); + + // log and continue if ping did not throw exception + socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, retrying"); + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java index afa3aa870ae..7dda8247018 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java @@ -52,6 +52,9 @@ default String type() { /** * How long to wait for the next HTTP/2 data frame to arrive in underlying stream. * Whether this is a fatal error or not is controlled by {@link #abortPollTimeExpired()}. + * If {@link #abortPollTimeExpired()} is set to {@code false}, the connection + * health will first be verified by attempting to send a PING frame before + * attempting a new read. * * @return poll time as a duration * @see io.helidon.common.socket.SocketOptions#readTimeout() diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index ccf0b78db71..5e9f6735410 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -40,6 +40,7 @@ import io.helidon.http.http2.Http2Headers; import io.helidon.http.http2.Http2HuffmanDecoder; import io.helidon.http.http2.Http2LoggingFrameListener; +import io.helidon.http.http2.Http2Ping; import io.helidon.http.http2.Http2Priority; import io.helidon.http.http2.Http2RstStream; import io.helidon.http.http2.Http2Setting; @@ -61,6 +62,8 @@ public class Http2ClientStream implements Http2Stream, ReleasableResource { private static final System.Logger LOGGER = System.getLogger(Http2ClientStream.class.getName()); private static final Set NON_CANCELABLE = Set.of(Http2StreamState.CLOSED, Http2StreamState.IDLE); + private static final Http2FrameData HTTP2_PING = Http2Ping.create().toFrameData(); + private final Http2ClientConnection connection; private final Http2Settings serverSettings; private final SocketContext ctx; @@ -343,6 +346,13 @@ public void writeData(BufferData entityBytes, boolean endOfStream) { splitAndWrite(frameData); } + /** + * Sends PING frame to server. Can be used to check if connection is healthy. + */ + public void sendPing() { + connection.writer().write(HTTP2_PING); + } + /** * Reads headers from this stream. * diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java new file mode 100644 index 00000000000..4e5edbca289 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java @@ -0,0 +1,95 @@ +/* + * 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.webclient.grpc.tests; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import io.grpc.stub.StreamObserver; +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webclient.grpc.GrpcClientMethodDescriptor; +import io.helidon.webclient.grpc.GrpcClientProtocolConfig; +import io.helidon.webclient.grpc.GrpcServiceDescriptor; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testing.junit5.ServerTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests server connection problems, such as a disappearing server. + */ +@ServerTest +class GrpcConnectionErrorTest extends GrpcBaseTest { + private static final long TIMEOUT_SECONDS = 1000; + + private final WebServer server; + private final GrpcClient grpcClient; + private final GrpcServiceDescriptor serviceDescriptor; + + private GrpcConnectionErrorTest(WebServer server) { + this.server = server; + Tls clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + GrpcClientProtocolConfig config = GrpcClientProtocolConfig.builder() + .pollWaitTime(Duration.ofSeconds(2)) // detects connection issues + .abortPollTimeExpired(false) // checks health with PING + .build(); + this.grpcClient = GrpcClient.builder() + .tls(clientTls) + .protocolConfig(config) + .baseUri("https://localhost:" + server.port()) + .build(); + this.serviceDescriptor = GrpcServiceDescriptor.builder() + .serviceName("StringService") + .putMethod("Join", + GrpcClientMethodDescriptor.clientStreaming("StringService", "Join") + .requestType(Strings.StringMessage.class) + .responseType(Strings.StringMessage.class) + .build()) + .build(); + } + + @Test + void testClientStreamingWithError() throws InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + StreamObserver req = grpcClient.serviceClient(serviceDescriptor) + .clientStream("Join", singleStreamObserver(future)); + req.onNext(newStringMessage("hello")); + server.stop(); // kill server! + assertEventually(future::isCompletedExceptionally, TIMEOUT_SECONDS * 1000); + } + + private static void assertEventually(Supplier predicate, long millis) throws InterruptedException { + long start = System.currentTimeMillis(); + do { + if (predicate.get()) { + return; + } + Thread.sleep(100); + } while (System.currentTimeMillis() - start <= millis); + fail("Predicate failed after " + millis + " milliseconds"); + } +} From 7fd5f7fd7296d36867c8040ea92994cdda945cb4 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Mon, 20 May 2024 16:46:25 -0400 Subject: [PATCH 33/38] Uses a separate virtual thread to check if the connection to the server has died. The period to check for that is configurable and set to 5 seconds by default. --- .../webclient/grpc/GrpcBaseClientCall.java | 7 +++ .../webclient/grpc/GrpcClientCall.java | 46 +++++++++++++------ .../GrpcClientProtocolConfigBlueprint.java | 13 ++++-- .../grpc/tests/GrpcConnectionErrorTest.java | 5 +- .../src/test/resources/logging.properties | 2 +- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index a7f7ead82f8..08b7c09d47e 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -62,6 +62,7 @@ abstract class GrpcBaseClientCall extends ClientCall { protected static final Header GRPC_ACCEPT_ENCODING = HeaderValues.create(HeaderNames.ACCEPT_ENCODING, "gzip"); protected static final Header GRPC_CONTENT_TYPE = HeaderValues.create(HeaderNames.CONTENT_TYPE, "application/grpc"); + protected static final BufferData PING_FRAME = BufferData.create("PING"); protected static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); private final GrpcClientImpl grpcClient; @@ -70,6 +71,7 @@ abstract class GrpcBaseClientCall extends ClientCall { private final int initBufferSize; private final Duration pollWaitTime; private final boolean abortPollTimeExpired; + private final Duration heartbeatPeriod; private final MethodDescriptor.Marshaller requestMarshaller; private final MethodDescriptor.Marshaller responseMarshaller; @@ -88,6 +90,11 @@ abstract class GrpcBaseClientCall extends ClientCall { this.initBufferSize = grpcClient.prototype().protocolConfig().initBufferSize(); this.pollWaitTime = grpcClient.prototype().protocolConfig().pollWaitTime(); this.abortPollTimeExpired = grpcClient.prototype().protocolConfig().abortPollTimeExpired(); + this.heartbeatPeriod = grpcClient.prototype().protocolConfig().heartbeatPeriod(); + } + + protected Duration heartbeatPeriod() { + return heartbeatPeriod; } protected boolean abortPollTimeExpired() { diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 70c51e1645c..550107e9060 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -16,6 +16,8 @@ package io.helidon.webclient.grpc; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -56,6 +58,7 @@ class GrpcClientCall extends GrpcBaseClientCall { private volatile Future readStreamFuture; private volatile Future writeStreamFuture; + private volatile Future heartbeatFuture; GrpcClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { super(grpcClient, methodDescriptor, callOptions); @@ -75,6 +78,7 @@ public void cancel(String message, Throwable cause) { responseListener().onClose(Status.CANCELLED, EMPTY_METADATA); readStreamFuture.cancel(true); writeStreamFuture.cancel(true); + heartbeatFuture.cancel(true); close(); } @@ -100,6 +104,29 @@ public void sendMessage(ReqT message) { } protected void startStreamingThreads() { + // heartbeat thread + Duration period = heartbeatPeriod(); + if (!period.isZero()) { + heartbeatFuture = executor.submit(() -> { + try { + startWriteBarrier.await(); + socket().log(LOGGER, DEBUG, "[Heartbeat thread] started with period " + period); + + while (isRemoteOpen()) { + Thread.sleep(period); + if (sendingQueue.isEmpty()) { + sendingQueue.add(PING_FRAME); + socket().log(LOGGER, DEBUG, "[Heartbeat thread] heartbeat queued"); + } + } + } catch (Throwable t) { + socket().log(LOGGER, DEBUG, "[Heartbeat thread] exception " + t.getMessage()); + } + }); + } else { + heartbeatFuture = CompletableFuture.completedFuture(null); + } + // write streaming thread writeStreamFuture = executor.submit(() -> { try { @@ -111,7 +138,11 @@ protected void startStreamingThreads() { socket().log(LOGGER, DEBUG, "[Writing thread] polling sending queue"); BufferData bufferData = sendingQueue.poll(pollWaitTime().toMillis(), TimeUnit.MILLISECONDS); if (bufferData != null) { - if (bufferData == EMPTY_BUFFER_DATA) { // end marker + if (bufferData == PING_FRAME) { // ping frame + clientStream().sendPing(); + continue; + } + if (bufferData == EMPTY_BUFFER_DATA) { // end marker socket().log(LOGGER, DEBUG, "[Writing thread] sending queue end marker found"); if (!endOfStream) { socket().log(LOGGER, DEBUG, "[Writing thread] sending empty buffer to end stream"); @@ -208,24 +239,11 @@ private void drainReceivingQueue() { } } - /** - * Handles a read timeout by either aborting or continuing, depending on how the client - * is configured. If not aborting, it will attempt to send a PING frame to check the - * connection health before attempting to proceed. - * - * @param e a stream timeout exception - */ private void handleStreamTimeout(StreamTimeoutException e) { - // abort or retry based on config settings if (abortPollTimeExpired()) { socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, aborting"); throw e; // caught below } - - // check connection health before proceeding - clientStream().sendPing(); - - // log and continue if ping did not throw exception socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, retrying"); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java index 7dda8247018..364dcaf5d24 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java @@ -52,9 +52,6 @@ default String type() { /** * How long to wait for the next HTTP/2 data frame to arrive in underlying stream. * Whether this is a fatal error or not is controlled by {@link #abortPollTimeExpired()}. - * If {@link #abortPollTimeExpired()} is set to {@code false}, the connection - * health will first be verified by attempting to send a PING frame before - * attempting a new read. * * @return poll time as a duration * @see io.helidon.common.socket.SocketOptions#readTimeout() @@ -74,6 +71,16 @@ default String type() { @Option.Default("false") boolean abortPollTimeExpired(); + /** + * How often to send a heartbeat (HTTP/2 ping) to check if the connection is still + * alive. Set the heartbeat to 0 to turn this feature off. + * + * @return heartbeat period + */ + @Option.Configured + @Option.Default("PT5S") + Duration heartbeatPeriod(); + /** * Initial buffer size used to serialize gRPC request payloads. Buffers shall grow * according to the payload size, but setting this initial buffer size to a larger value diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java index 4e5edbca289..27862b5a56f 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConnectionErrorTest.java @@ -38,7 +38,7 @@ */ @ServerTest class GrpcConnectionErrorTest extends GrpcBaseTest { - private static final long TIMEOUT_SECONDS = 1000; + private static final long TIMEOUT_SECONDS = 10; private final WebServer server; private final GrpcClient grpcClient; @@ -54,8 +54,7 @@ private GrpcConnectionErrorTest(WebServer server) { .keystore(Resource.create("client.p12")))) .build(); GrpcClientProtocolConfig config = GrpcClientProtocolConfig.builder() - .pollWaitTime(Duration.ofSeconds(2)) // detects connection issues - .abortPollTimeExpired(false) // checks health with PING + .heartbeatPeriod(Duration.ofSeconds(1)) // detects failure faster .build(); this.grpcClient = GrpcClient.builder() .tls(clientTls) diff --git a/webclient/tests/grpc/src/test/resources/logging.properties b/webclient/tests/grpc/src/test/resources/logging.properties index 09e74ff2922..344ba7085fd 100644 --- a/webclient/tests/grpc/src/test/resources/logging.properties +++ b/webclient/tests/grpc/src/test/resources/logging.properties @@ -21,4 +21,4 @@ handlers=io.helidon.logging.jul.HelidonConsoleHandler java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n .level=INFO -io.helidon.webclient.grpc.level=FINEST +#io.helidon.webclient.grpc.level=FINEST From 5bf8f9e5ac9fc008f5a36c79c5f899360bff43e3 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 21 May 2024 10:56:02 -0400 Subject: [PATCH 34/38] Sets heartbeat period to 0, thus disabling the feature by default. Signed-off-by: Santiago Pericas-Geertsen --- .../webclient/grpc/GrpcClientProtocolConfigBlueprint.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java index 364dcaf5d24..bb83ca08017 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientProtocolConfigBlueprint.java @@ -73,12 +73,13 @@ default String type() { /** * How often to send a heartbeat (HTTP/2 ping) to check if the connection is still - * alive. Set the heartbeat to 0 to turn this feature off. + * alive. This is useful for long-running, streaming gRPC calls. It is turned off by + * default but can be enabled by setting the period to a value greater than 0. * * @return heartbeat period */ @Option.Configured - @Option.Default("PT5S") + @Option.Default("PT0S") Duration heartbeatPeriod(); /** From 765fed9efda5e83a41efa4c17c39d1116ba0e560 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 21 May 2024 14:13:42 -0400 Subject: [PATCH 35/38] Fixes logging message and test. --- .../main/java/io/helidon/webclient/grpc/GrpcClientCall.java | 2 +- .../tests/{GrpcConfigTest.java => GrpcConfigProtocolTest.java} | 2 ++ webclient/tests/grpc/src/test/resources/application.yaml | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) rename webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/{GrpcConfigTest.java => GrpcConfigProtocolTest.java} (92%) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 550107e9060..e8ca4b66726 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -116,7 +116,6 @@ protected void startStreamingThreads() { Thread.sleep(period); if (sendingQueue.isEmpty()) { sendingQueue.add(PING_FRAME); - socket().log(LOGGER, DEBUG, "[Heartbeat thread] heartbeat queued"); } } } catch (Throwable t) { @@ -140,6 +139,7 @@ protected void startStreamingThreads() { if (bufferData != null) { if (bufferData == PING_FRAME) { // ping frame clientStream().sendPing(); + socket().log(LOGGER, DEBUG, "[Writing thread] heartbeat sent"); continue; } if (bufferData == EMPTY_BUFFER_DATA) { // end marker diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigProtocolTest.java similarity index 92% rename from webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java rename to webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigProtocolTest.java index dd26f4c4821..86125518fc0 100644 --- a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigTest.java +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcConfigProtocolTest.java @@ -37,6 +37,7 @@ void testDefaults() { assertThat(config.pollWaitTime(), is(Duration.ofSeconds(10))); assertThat(config.abortPollTimeExpired(), is(false)); assertThat(config.initBufferSize(), is(2048)); + assertThat(config.heartbeatPeriod(), is(Duration.ofSeconds(0))); } @Test @@ -46,5 +47,6 @@ void testApplicationConfig() { assertThat(config.pollWaitTime(), is(Duration.ofSeconds(30))); assertThat(config.abortPollTimeExpired(), is(true)); assertThat(config.initBufferSize(), is(10000)); + assertThat(config.heartbeatPeriod(), is(Duration.ofSeconds(10))); } } diff --git a/webclient/tests/grpc/src/test/resources/application.yaml b/webclient/tests/grpc/src/test/resources/application.yaml index fd5df3d4369..84409629c2a 100644 --- a/webclient/tests/grpc/src/test/resources/application.yaml +++ b/webclient/tests/grpc/src/test/resources/application.yaml @@ -17,4 +17,5 @@ grpc-client: poll-wait-time: PT30S abort-poll-time-expired: true - init-buffer-size: 10000 \ No newline at end of file + init-buffer-size: 10000 + heartbeat-period: PT10S \ No newline at end of file From 1146ef5f8104534fa3dbac80cd073b5d51b7a458 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Tue, 28 May 2024 15:29:23 -0400 Subject: [PATCH 36/38] Refactors a few classes in grpc/core. Addresses other comments in PR review. --- grpc/core/pom.xml | 16 +-- .../grpc/core/DefaultMarshallerSupplier.java | 45 +++++++ .../helidon/grpc/core/MarshallerSupplier.java | 52 +------- .../grpc/core/ProtoMarshallerSupplier.java | 50 ++++++++ .../io/helidon/grpc/core/WeightedBag.java | 111 +++++++----------- grpc/core/src/main/java/module-info.java | 5 +- .../io/helidon/grpc/core/WeightedBagTest.java | 41 ++++--- grpc/pom.xml | 15 +++ .../webclient/grpc/GrpcBaseClientCall.java | 64 +++++----- .../webclient/grpc/GrpcClientCall.java | 2 +- .../webclient/grpc/GrpcClientImpl.java | 4 +- .../grpc/GrpcClientMethodDescriptor.java | 6 +- 12 files changed, 227 insertions(+), 184 deletions(-) create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java create mode 100644 grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml index 05b9616a70a..a1a059712c2 100644 --- a/grpc/core/pom.xml +++ b/grpc/core/pom.xml @@ -31,11 +31,7 @@ io.helidon.common - helidon-common-context - - - io.helidon.common - helidon-common-config + helidon-common io.grpc @@ -43,11 +39,11 @@ io.grpc - grpc-core + grpc-protobuf - io.grpc - grpc-protobuf + com.google.protobuf + protobuf-java io.grpc @@ -59,10 +55,6 @@ - - jakarta.annotation - jakarta.annotation-api - org.junit.jupiter junit-jupiter-api diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java new file mode 100644 index 00000000000..fa37552cdcb --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java @@ -0,0 +1,45 @@ +/* + * 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.grpc.core; + +import com.google.protobuf.MessageLite; +import io.grpc.MethodDescriptor; + +/** + * The default {@link MarshallerSupplier}. + */ +class DefaultMarshallerSupplier implements MarshallerSupplier { + + static DefaultMarshallerSupplier create() { + return new DefaultMarshallerSupplier(); + } + + private DefaultMarshallerSupplier() { + } + + private final ProtoMarshallerSupplier proto = ProtoMarshallerSupplier.create(); + + @Override + public MethodDescriptor.Marshaller get(Class clazz) { + if (MessageLite.class.isAssignableFrom(clazz)) { + return proto.get(clazz); + } + String msg = String.format( + "Class %s must be a valid ProtoBuf message, or a custom marshaller for it must be specified explicitly", + clazz.getName()); + throw new IllegalArgumentException(msg); + } +} diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java index a0caade5e36..625d777f775 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -16,9 +16,7 @@ package io.helidon.grpc.core; -import com.google.protobuf.MessageLite; import io.grpc.MethodDescriptor; -import io.grpc.protobuf.lite.ProtoLiteUtils; /** * A supplier of {@link MethodDescriptor.Marshaller} instances for specific @@ -38,53 +36,11 @@ public interface MarshallerSupplier { MethodDescriptor.Marshaller get(Class clazz); /** - * Obtain the default marshaller. + * Creates a default marshaller supplier. * - * @return the default marshaller + * @return the default marshaller supplier */ - static MarshallerSupplier defaultInstance() { - return new DefaultMarshallerSupplier(); - } - - /** - * The default {@link MarshallerSupplier}. - */ - class DefaultMarshallerSupplier implements MarshallerSupplier { - - private final ProtoMarshallerSupplier proto = new ProtoMarshallerSupplier(); - - @Override - public MethodDescriptor.Marshaller get(Class clazz) { - if (MessageLite.class.isAssignableFrom(clazz)) { - return proto.get(clazz); - } - String msg = String.format( - "Class %s must be a valid ProtoBuf message, or a custom marshaller for it must be specified explicitly", - clazz.getName()); - throw new IllegalArgumentException(msg); - } - } - - /** - * A {@link MarshallerSupplier} implementation that - * supplies Protocol Buffer marshaller instances. - */ - class ProtoMarshallerSupplier implements MarshallerSupplier { - - @Override - @SuppressWarnings("unchecked") - public MethodDescriptor.Marshaller get(Class clazz) { - try { - java.lang.reflect.Method getDefaultInstance = clazz.getDeclaredMethod("getDefaultInstance"); - MessageLite instance = (MessageLite) getDefaultInstance.invoke(clazz); - - return (MethodDescriptor.Marshaller) ProtoLiteUtils.marshaller(instance); - } catch (Exception e) { - String msg = String.format( - "Attempting to use class %s, which is not a valid Protocol buffer message, with a default marshaller", - clazz.getName()); - throw new IllegalArgumentException(msg); - } - } + static MarshallerSupplier create() { + return DefaultMarshallerSupplier.create(); } } diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java new file mode 100644 index 00000000000..740d089709e --- /dev/null +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java @@ -0,0 +1,50 @@ +/* + * 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.grpc.core; + +import com.google.protobuf.Message; +import io.grpc.MethodDescriptor; +import io.grpc.protobuf.ProtoUtils; + +/** + * A {@link MarshallerSupplier} implementation that supplies Protocol Buffer + * marshaller instances. + */ +class ProtoMarshallerSupplier implements MarshallerSupplier { + + static ProtoMarshallerSupplier create() { + return new ProtoMarshallerSupplier(); + } + + private ProtoMarshallerSupplier() { + } + + @Override + @SuppressWarnings("unchecked") + public MethodDescriptor.Marshaller get(Class clazz) { + try { + java.lang.reflect.Method getDefaultInstance = clazz.getDeclaredMethod("getDefaultInstance"); + Message instance = (Message) getDefaultInstance.invoke(clazz); + return (MethodDescriptor.Marshaller) ProtoUtils.marshaller(instance); + } catch (Exception e) { + String msg = String.format( + "Attempting to use class %s, which is not a valid Protocol buffer message, with a default marshaller", + clazz.getName()); + throw new IllegalArgumentException(msg); + } + } +} + diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java index 202cc20c92f..f8d5d8864ff 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java @@ -21,6 +21,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.TreeMap; import java.util.stream.Stream; @@ -28,13 +29,10 @@ import io.helidon.common.Weighted; /** - * A bag of values ordered by weight. + * A bag of values ordered by weight. All weights must be greater or equal to 0. *

- * The higher the weight the higher the weight. For cases where weight is the same, + * The higher the weight the higher the priority. For cases where the weight is the same, * elements are returned in the order that they were added to the bag. - *

- * Elements added with negative weights are assumed to have no weight and - * will be least significant in order. * * @param the type of elements in the bag * @see io.helidon.common.Weight @@ -42,44 +40,41 @@ public class WeightedBag implements Iterable { private final Map> contents; - private final List noWeightedList; private final double defaultWeight; - private WeightedBag(Map> contents, List noweightList, double defaultWeight) { + private WeightedBag(Map> contents, double defaultWeight) { this.contents = contents; - this.noWeightedList = noweightList; + if (defaultWeight < 0.0) { + throw new IllegalArgumentException("Weights must be greater or equal to 0"); + } this.defaultWeight = defaultWeight; } /** - * Create a new {@link WeightedBag} where elements added with no weight will be last - * in the order. + * Create a new {@link WeightedBag} where elements added with no weight will be + * given a default weight of 0. * * @param the type of elements in the bag - * @return a new {@link WeightedBag} where elements - * dded with no weight will be last in the - * order + * @return a new {@link WeightedBag} where elements added + * with no weight will be given {@link Weighted#DEFAULT_WEIGHT} */ public static WeightedBag create() { - return withDefaultWeight(-1.0); + return create(Weighted.DEFAULT_WEIGHT); } /** - * Create a new {@link WeightedBag} where elements added with no weight will be given - * a default weight value. - * - * @param weight the default weight value to assign - * to elements added with no weight - * @param the type of elements in the bag + * Create a new {@link WeightedBag} where elements added with no weight will be + * given a default weight of 0. * - * @return a new {@link WeightedBag} where elements - * added with no weight will be given - * a default weight value + * @param defaultWeight default weight for elements + * @param the type of elements in the bag + * @return a new {@link WeightedBag} where elements added + * with no weight will be given {@link Weighted#DEFAULT_WEIGHT} */ - public static WeightedBag withDefaultWeight(double weight) { + public static WeightedBag create(double defaultWeight) { return new WeightedBag<>(new TreeMap<>( (o1, o2) -> Double.compare(o2, o1)), // reversed for weights - new ArrayList<>(), weight); + defaultWeight); } /** @@ -88,18 +83,7 @@ public static WeightedBag withDefaultWeight(double weight) { * @return outcome of test */ public boolean isEmpty() { - return contents.isEmpty() && noWeightedList.isEmpty(); - } - - /** - * Obtain a copy of this {@link WeightedBag}. - * - * @return a copy of this {@link WeightedBag} - */ - public WeightedBag copyMe() { - WeightedBag copy = WeightedBag.create(); - copy.merge(this); - return copy; + return contents.isEmpty(); } /** @@ -108,9 +92,7 @@ public WeightedBag copyMe() { * @return an immutable copy of this {@link WeightedBag} */ public WeightedBag readOnly() { - return new WeightedBag<>(Collections.unmodifiableMap(contents), - Collections.unmodifiableList(noWeightedList), - defaultWeight); + return new WeightedBag<>(Collections.unmodifiableMap(contents), defaultWeight); } /** @@ -120,7 +102,6 @@ public WeightedBag readOnly() { */ public void merge(WeightedBag bag) { bag.contents.forEach((weight, value) -> addAll(value, weight)); - this.noWeightedList.addAll(bag.noWeightedList); } /** @@ -139,7 +120,7 @@ public void addAll(Iterable values) { } /** - * Add elements to the bag. + * Add elements to the bag with a given weight. * * @param values the elements to add * @param weight the weight to assign to the elements @@ -160,16 +141,15 @@ public void addAll(Iterable values, double weight) { * @param value the element to add */ public void add(T value) { - if (value != null) { - double weight; - if (value instanceof Weighted weighted) { - weight = weighted.weight(); - } else { - Weight annotation = value.getClass().getAnnotation(Weight.class); - weight = annotation == null ? defaultWeight : annotation.value(); - } - add(value, weight); + Objects.requireNonNull(value); + double weight; + if (value instanceof Weighted weighted) { + weight = weighted.weight(); + } else { + Weight annotation = value.getClass().getAnnotation(Weight.class); + weight = (annotation == null) ? defaultWeight : annotation.value(); } + add(value, weight); } /** @@ -179,13 +159,18 @@ public void add(T value) { * @param weight the weight of the element */ public void add(T value, double weight) { - if (value != null) { - if (weight < 0.0) { - noWeightedList.add(value); - } else { - contents.compute(weight, (key, list) -> combine(list, value)); - } + Objects.requireNonNull(value); + if (weight < 0.0) { + throw new IllegalArgumentException("Weights must be greater or equal to 0"); } + contents.compute(weight, (key, list) -> { + List newList = list; + if (newList == null) { + newList = new ArrayList<>(); + } + newList.add(value); + return newList; + }); } /** @@ -196,23 +181,13 @@ public void add(T value, double weight) { * an ordered {@link Stream} */ public Stream stream() { - Stream stream = contents.entrySet() + return contents.entrySet() .stream() .flatMap(e -> e.getValue().stream()); - - return Stream.concat(stream, noWeightedList.stream()); } @Override public Iterator iterator() { return stream().iterator(); } - - private List combine(List list, T value) { - if (list == null) { - list = new ArrayList<>(); - } - list.add(value); - return list; - } } diff --git a/grpc/core/src/main/java/module-info.java b/grpc/core/src/main/java/module-info.java index 6fe54b37889..695cf325613 100644 --- a/grpc/core/src/main/java/module-info.java +++ b/grpc/core/src/main/java/module-info.java @@ -19,15 +19,12 @@ */ module io.helidon.grpc.core { - requires transitive io.helidon.common.context; - requires transitive io.helidon.common.config; + requires io.helidon.common; requires transitive io.grpc; requires transitive io.grpc.stub; requires transitive com.google.protobuf; requires transitive io.grpc.protobuf; requires transitive io.grpc.protobuf.lite; - requires jakarta.annotation; - exports io.helidon.grpc.core; } diff --git a/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java b/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java index 841952dc2e8..5000a3065c9 100644 --- a/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java +++ b/grpc/core/src/test/java/io/helidon/grpc/core/WeightedBagTest.java @@ -24,11 +24,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.jupiter.api.Assertions.assertThrows; class WeightedBagTest { @Test - public void shouldReturnElementsInOrder() { + void shouldReturnElementsInOrder() { WeightedBag bag = WeightedBag.create(); bag.add("Three", 3.0); bag.add("Two", 2.0); @@ -37,7 +38,7 @@ public void shouldReturnElementsInOrder() { } @Test - public void shouldReturnElementsInOrderWithinSameWeight() { + void shouldReturnElementsInOrderWithinSameWeight() { WeightedBag bag = WeightedBag.create(); bag.add("Two", 2.0); bag.add("TwoToo", 2.0); @@ -45,16 +46,16 @@ public void shouldReturnElementsInOrderWithinSameWeight() { } @Test - public void shouldReturnNoWeightElementsLast() { + void shouldReturnNoWeightElementsLast() { WeightedBag bag = WeightedBag.create(); - bag.add("Three", 3.0); - bag.add("Last"); - bag.add("One", 1.0); + bag.add("Three", 300.0); + bag.add("Last"); // default weight 100 + bag.add("One", 200.0); assertThat(bag, contains("Three", "One", "Last")); } @Test - public void shouldGetWeightFromAnnotation() { + void shouldGetWeightFromAnnotation() { WeightedBag bag = WeightedBag.create(); Value value = new Value(); bag.add("One", 1.0); @@ -64,7 +65,7 @@ public void shouldGetWeightFromAnnotation() { } @Test - public void shouldGetWeightFromWeighted() { + void shouldGetWeightFromWeighted() { WeightedBag bag = WeightedBag.create(); WeightedValue value = new WeightedValue(); bag.add("One", 1.0); @@ -74,7 +75,7 @@ public void shouldGetWeightFromWeighted() { } @Test - public void shouldUseWeightFromWeightedOverAnnotation() { + void shouldUseWeightFromWeightedOverAnnotation() { WeightedBag bag = WeightedBag.create(); AnnotatedWeightedValue value = new AnnotatedWeightedValue(); bag.add("One", 1.0); @@ -84,8 +85,8 @@ public void shouldUseWeightFromWeightedOverAnnotation() { } @Test - public void shouldUseDefaultWeight() { - WeightedBag bag = WeightedBag.withDefaultWeight(2); + void shouldUseDefaultWeight() { + WeightedBag bag = WeightedBag.create(2.0); bag.add("One", 1.0); bag.add("Three", 3.0); bag.add("Two"); @@ -93,14 +94,14 @@ public void shouldUseDefaultWeight() { } @Test - public void shouldAddAll() { + void shouldAddAll() { WeightedBag bag = WeightedBag.create(); bag.addAll(Arrays.asList("Three", "Two", "One")); assertThat(bag, contains("Three", "Two", "One")); } @Test - public void shouldAddAllWithWeight() { + void shouldAddAllWithWeight() { WeightedBag bag = WeightedBag.create(); bag.add("First", 1.0); bag.add("Last", 3.0); @@ -109,7 +110,7 @@ public void shouldAddAllWithWeight() { } @Test - public void shouldMerge() { + void shouldMerge() { WeightedBag bagOne = WeightedBag.create(); WeightedBag bagTwo = WeightedBag.create(); @@ -127,6 +128,18 @@ public void shouldMerge() { assertThat(bagOne, contains("H", "D", "F", "G", "B", "C", "A", "E")); } + @Test + void badValue() { + WeightedBag bag = WeightedBag.create(); + assertThrows(NullPointerException.class, () -> bag.add(null, 1.0)); + } + + @Test + void badWeight() { + WeightedBag bag = WeightedBag.create(); + assertThrows(IllegalArgumentException.class, () -> bag.add("First", -1.0)); + } + @Weight(2.0) public static class Value { } diff --git a/grpc/pom.xml b/grpc/pom.xml index 64b1c84c1a2..2bbfdce4d6e 100644 --- a/grpc/pom.xml +++ b/grpc/pom.xml @@ -35,4 +35,19 @@ core + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + check-dependencies + verify + + + + + diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 08b7c09d47e..65d5a1fcab1 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -93,38 +93,6 @@ abstract class GrpcBaseClientCall extends ClientCall { this.heartbeatPeriod = grpcClient.prototype().protocolConfig().heartbeatPeriod(); } - protected Duration heartbeatPeriod() { - return heartbeatPeriod; - } - - protected boolean abortPollTimeExpired() { - return abortPollTimeExpired; - } - - protected Duration pollWaitTime() { - return pollWaitTime; - } - - protected Http2ClientConnection connection() { - return connection; - } - - protected MethodDescriptor.Marshaller requestMarshaller() { - return requestMarshaller; - } - - protected GrpcClientStream clientStream() { - return clientStream; - } - - protected Listener responseListener() { - return responseListener; - } - - protected HelidonSocket socket() { - return socket; - } - @Override public void start(Listener responseListener, Metadata metadata) { LOGGER.log(DEBUG, "start called"); @@ -244,4 +212,36 @@ protected byte[] serializeMessage(ReqT message) { } return baos.toByteArray(); } + + protected Duration heartbeatPeriod() { + return heartbeatPeriod; + } + + protected boolean abortPollTimeExpired() { + return abortPollTimeExpired; + } + + protected Duration pollWaitTime() { + return pollWaitTime; + } + + protected Http2ClientConnection connection() { + return connection; + } + + protected MethodDescriptor.Marshaller requestMarshaller() { + return requestMarshaller; + } + + protected GrpcClientStream clientStream() { + return clientStream; + } + + protected Listener responseListener() { + return responseListener; + } + + protected HelidonSocket socket() { + return socket; + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index e8ca4b66726..101f8599114 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -242,7 +242,7 @@ private void drainReceivingQueue() { private void handleStreamTimeout(StreamTimeoutException e) { if (abortPollTimeExpired()) { socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, aborting"); - throw e; // caught below + throw e; } socket().log(LOGGER, ERROR, "[Reading thread] HTTP/2 stream timeout, retrying"); } 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 a30d01828ab..7a8ce6dfa91 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 @@ -34,11 +34,11 @@ class GrpcClientImpl implements GrpcClient { this.clientConfig = clientConfig; } - public WebClient webClient() { + WebClient webClient() { return webClient; } - public Http2Client http2Client() { + Http2Client http2Client() { return http2Client; } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index b28170216cc..41b0ffc1f2a 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -315,8 +315,8 @@ public static class Builder private final MethodDescriptor.Builder descriptor; private Class requestType; private Class responseType; - private final WeightedBag interceptors = WeightedBag.withDefaultWeight(InterceptorWeights.USER); - private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.defaultInstance(); + private final WeightedBag interceptors = WeightedBag.create(InterceptorWeights.USER); + private MarshallerSupplier defaultMarshallerSupplier = MarshallerSupplier.create(); private MarshallerSupplier marshallerSupplier; private CallCredentials callCredentials; private MethodHandler methodHandler; @@ -364,7 +364,7 @@ public Builder marshallerSupplier(MarshallerSupplier supplier) { } Builder defaultMarshallerSupplier(MarshallerSupplier supplier) { - this.defaultMarshallerSupplier = Objects.requireNonNullElseGet(supplier, MarshallerSupplier::defaultInstance); + this.defaultMarshallerSupplier = Objects.requireNonNullElseGet(supplier, MarshallerSupplier::create); return this; } From b6f57d207de0e1ec99ac1654cb11b85a97f4cce6 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 29 May 2024 12:28:25 -0400 Subject: [PATCH 37/38] Reduces the number of logging statements in favor of using low-level HTTP/2 logging instead. Signed-off-by: Santiago Pericas-Geertsen --- .../main/java/io/helidon/webclient/grpc/GrpcClientCall.java | 6 ------ .../java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java | 2 -- 2 files changed, 8 deletions(-) diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 101f8599114..96573554350 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -91,8 +91,6 @@ public void halfClose() { @Override public void sendMessage(ReqT message) { - socket().log(LOGGER, DEBUG, "sendMessage called"); - // serialize and queue message for writing byte[] serialized = serializeMessage(message); BufferData messageData = BufferData.createReadOnly(serialized, 0, serialized.length); @@ -139,13 +137,10 @@ protected void startStreamingThreads() { if (bufferData != null) { if (bufferData == PING_FRAME) { // ping frame clientStream().sendPing(); - socket().log(LOGGER, DEBUG, "[Writing thread] heartbeat sent"); continue; } if (bufferData == EMPTY_BUFFER_DATA) { // end marker - socket().log(LOGGER, DEBUG, "[Writing thread] sending queue end marker found"); if (!endOfStream) { - socket().log(LOGGER, DEBUG, "[Writing thread] sending empty buffer to end stream"); clientStream().writeData(EMPTY_BUFFER_DATA, true); } break; @@ -234,7 +229,6 @@ private void drainReceivingQueue() { while (messageRequest.get() > 0 && !receivingQueue.isEmpty()) { messageRequest.getAndDecrement(); ResT res = toResponse(receivingQueue.remove()); - socket().log(LOGGER, DEBUG, "[Reading thread] sending response to listener"); responseListener().onMessage(res); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index d0cde6fdcdb..d46da45be2b 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -69,8 +69,6 @@ public void halfClose() { @Override public void sendMessage(ReqT message) { - socket().log(LOGGER, DEBUG, "sendMessage called"); - // should only be called once if (requestSent) { close(Status.FAILED_PRECONDITION); From a1995d9c5dfeb16abcf58cd6f942e140731ce548 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Thu, 30 May 2024 08:56:56 -0400 Subject: [PATCH 38/38] Some code reformatting using the Helidon scheme. Signed-off-by: Santiago Pericas-Geertsen --- .../grpc/core/DefaultMarshallerSupplier.java | 6 +-- .../helidon/grpc/core/MarshallerSupplier.java | 5 +- .../io/helidon/grpc/core/MethodHandler.java | 52 +++++++++--------- .../grpc/core/ProtoMarshallerSupplier.java | 6 +-- .../io/helidon/grpc/core/WeightedBag.java | 18 +++---- .../webclient/grpc/GrpcBaseClientCall.java | 3 +- .../io/helidon/webclient/grpc/GrpcClient.java | 6 +-- .../grpc/GrpcClientMethodDescriptor.java | 54 +++++++++---------- .../webclient/grpc/GrpcClientStream.java | 10 ++-- .../webclient/grpc/GrpcProtocolProvider.java | 8 +-- .../webclient/grpc/GrpcServiceClient.java | 12 ++--- .../webclient/grpc/GrpcUnaryClientCall.java | 5 +- 12 files changed, 94 insertions(+), 91 deletions(-) diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java index fa37552cdcb..8a3a0afe266 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/DefaultMarshallerSupplier.java @@ -23,11 +23,11 @@ */ class DefaultMarshallerSupplier implements MarshallerSupplier { - static DefaultMarshallerSupplier create() { - return new DefaultMarshallerSupplier(); + private DefaultMarshallerSupplier() { } - private DefaultMarshallerSupplier() { + static DefaultMarshallerSupplier create() { + return new DefaultMarshallerSupplier(); } private final ProtoMarshallerSupplier proto = ProtoMarshallerSupplier.create(); diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java index 625d777f775..7a2d3ba2bcb 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MarshallerSupplier.java @@ -28,9 +28,8 @@ public interface MarshallerSupplier { /** * Obtain a {@link MethodDescriptor.Marshaller} for a type. * - * @param clazz the {@link Class} of the type to obtain the {@link MethodDescriptor.Marshaller} for - * @param the type to be marshalled - * + * @param clazz the {@link Class} of the type to obtain the {@link MethodDescriptor.Marshaller} for + * @param the type to be marshalled * @return a {@link MethodDescriptor.Marshaller} for a type */ MethodDescriptor.Marshaller get(Class clazz); diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java b/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java index fd898b0e1a2..6c0d238c719 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/MethodHandler.java @@ -25,32 +25,34 @@ /** * A gRPC method call handler. * - * @param the request type + * @param the request type * @param the response type */ public interface MethodHandler extends ServerCalls.UnaryMethod, - ServerCalls.ClientStreamingMethod, - ServerCalls.ServerStreamingMethod, - ServerCalls.BidiStreamingMethod { + ServerCalls.ClientStreamingMethod, + ServerCalls.ServerStreamingMethod, + ServerCalls.BidiStreamingMethod { /** * Obtain the {@link MethodDescriptor.MethodType gRPC method tyoe} that * this {@link MethodHandler} handles. * * @return the {@link MethodDescriptor.MethodType gRPC method type} that - * this {@link MethodHandler} handles + * this {@link MethodHandler} handles */ MethodDescriptor.MethodType type(); /** * Obtain the request type. - * @return the request type + * + * @return the request type */ Class getRequestType(); /** * Obtain the response type. - * @return the response type + * + * @return the response type */ Class getResponseType(); @@ -64,7 +66,7 @@ public interface MethodHandler /** * Determine whether this is a client side only handler. * - * @return {@code true} if this handler can only be used on the client + * @return {@code true} if this handler can only be used on the client */ default boolean clientOnly() { return false; @@ -132,10 +134,10 @@ interface BidirectionalClient { /** * Perform a bidirectional client call. * - * @param methodName the name of the gRPC method - * @param observer the {@link StreamObserver} that will receive the responses - * @param the request type - * @param the response type + * @param methodName the name of the gRPC method + * @param observer the {@link StreamObserver} that will receive the responses + * @param the request type + * @param the response type * @return a {@link StreamObserver} to use to send requests */ StreamObserver bidiStreaming(String methodName, StreamObserver observer); @@ -148,10 +150,10 @@ interface ClientStreaming { /** * Perform a client streaming client call. * - * @param methodName the name of the gRPC method - * @param observer the {@link StreamObserver} that will receive the responses - * @param the request type - * @param the response type + * @param methodName the name of the gRPC method + * @param observer the {@link StreamObserver} that will receive the responses + * @param the request type + * @param the response type * @return a {@link StreamObserver} to use to send requests */ StreamObserver clientStreaming(String methodName, StreamObserver observer); @@ -164,11 +166,11 @@ interface ServerStreamingClient { /** * Perform a server streaming client call. * - * @param methodName the name of the gRPC method - * @param request the request message - * @param observer the {@link StreamObserver} that will receive the responses - * @param the request type - * @param the response type + * @param methodName the name of the gRPC method + * @param request the request message + * @param observer the {@link StreamObserver} that will receive the responses + * @param the request type + * @param the response type */ void serverStreaming(String methodName, ReqT request, StreamObserver observer); } @@ -180,10 +182,10 @@ interface UnaryClient { /** * Perform a unary client call. * - * @param methodName the name of the gRPC method - * @param request the request message - * @param the request type - * @param the response type + * @param methodName the name of the gRPC method + * @param request the request message + * @param the request type + * @param the response type * @return a {@link java.util.concurrent.CompletableFuture} that completes when the call completes */ CompletionStage unary(String methodName, ReqT request); diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java b/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java index 740d089709e..6dff325413b 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/ProtoMarshallerSupplier.java @@ -25,11 +25,11 @@ */ class ProtoMarshallerSupplier implements MarshallerSupplier { - static ProtoMarshallerSupplier create() { - return new ProtoMarshallerSupplier(); + private ProtoMarshallerSupplier() { } - private ProtoMarshallerSupplier() { + static ProtoMarshallerSupplier create() { + return new ProtoMarshallerSupplier(); } @Override diff --git a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java index f8d5d8864ff..6199ab63ff8 100644 --- a/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java +++ b/grpc/core/src/main/java/io/helidon/grpc/core/WeightedBag.java @@ -56,7 +56,7 @@ private WeightedBag(Map> contents, double defaultWeight) { * * @param the type of elements in the bag * @return a new {@link WeightedBag} where elements added - * with no weight will be given {@link Weighted#DEFAULT_WEIGHT} + * with no weight will be given {@link Weighted#DEFAULT_WEIGHT} */ public static WeightedBag create() { return create(Weighted.DEFAULT_WEIGHT); @@ -69,7 +69,7 @@ public static WeightedBag create() { * @param defaultWeight default weight for elements * @param the type of elements in the bag * @return a new {@link WeightedBag} where elements added - * with no weight will be given {@link Weighted#DEFAULT_WEIGHT} + * with no weight will be given {@link Weighted#DEFAULT_WEIGHT} */ public static WeightedBag create(double defaultWeight) { return new WeightedBag<>(new TreeMap<>( @@ -98,7 +98,7 @@ public WeightedBag readOnly() { /** * Merge a {@link WeightedBag} into this {@link WeightedBag}. * - * @param bag the bag to merge + * @param bag the bag to merge */ public void merge(WeightedBag bag) { bag.contents.forEach((weight, value) -> addAll(value, weight)); @@ -111,7 +111,7 @@ public void merge(WeightedBag bag) { * annotation then that value will be used to determine weight otherwise the * default weight value will be used. * - * @param values the elements to add + * @param values the elements to add */ public void addAll(Iterable values) { for (T value : values) { @@ -122,8 +122,8 @@ public void addAll(Iterable values) { /** * Add elements to the bag with a given weight. * - * @param values the elements to add - * @param weight the weight to assign to the elements + * @param values the elements to add + * @param weight the weight to assign to the elements */ public void addAll(Iterable values, double weight) { for (T value : values) { @@ -138,7 +138,7 @@ public void addAll(Iterable values, double weight) { * annotation then that value will be used to determine weight otherwise the * default weight value will be used. * - * @param value the element to add + * @param value the element to add */ public void add(T value) { Objects.requireNonNull(value); @@ -155,7 +155,7 @@ public void add(T value) { /** * Add an element to the bag with a specific weight. * - * @param value the element to add + * @param value the element to add * @param weight the weight of the element */ public void add(T value, double weight) { @@ -178,7 +178,7 @@ public void add(T value, double weight) { * an ordered {@link Stream}. * * @return the contents of this {@link WeightedBag} as - * an ordered {@link Stream} + * an ordered {@link Stream} */ public Stream stream() { return contents.entrySet() diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 65d5a1fcab1..65ebfced73d 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -157,7 +157,8 @@ protected void unblockUnaryExecutor() { Executor executor = callOptions.getExecutor(); if (executor != null) { try { - executor.execute(() -> {}); + executor.execute(() -> { + }); } catch (Throwable t) { // ignored } 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 6fb03c4d040..d786f7840f0 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 @@ -69,8 +69,8 @@ static GrpcClient create(GrpcClientConfig clientConfig) { */ static GrpcClient create(Consumer consumer) { return create(GrpcClientConfig.builder() - .update(consumer) - .buildPrototype()); + .update(consumer) + .buildPrototype()); } /** @@ -112,6 +112,6 @@ static GrpcClient create() { * @return a new gRPC channel */ default Channel channel(Collection interceptors) { - return channel(interceptors.toArray(new ClientInterceptor[] {})); + return channel(interceptors.toArray(new ClientInterceptor[]{})); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java index 41b0ffc1f2a..7585b6944cb 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientMethodDescriptor.java @@ -84,13 +84,13 @@ private GrpcClientMethodDescriptor(String name, * specified name and {@link io.grpc.MethodDescriptor}. * * @param serviceName the name of the owning gRPC service - * @param name the simple method name - * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} + * @param name the simple method name + * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} * @return A new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static Builder builder(String serviceName, - String name, - MethodDescriptor.Builder descriptor) { + String name, + MethodDescriptor.Builder descriptor) { return new Builder(serviceName, name, descriptor); } @@ -99,13 +99,13 @@ public static Builder builder(String serviceName, * specified name and {@link io.grpc.MethodDescriptor}. * * @param serviceName the name of the owning gRPC service - * @param name the simple method name - * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} + * @param name the simple method name + * @param descriptor the underlying gRPC {@link io.grpc.MethodDescriptor.Builder} * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static GrpcClientMethodDescriptor create(String serviceName, - String name, - MethodDescriptor.Builder descriptor) { + String name, + MethodDescriptor.Builder descriptor) { return builder(serviceName, name, descriptor).build(); } @@ -114,7 +114,7 @@ public static GrpcClientMethodDescriptor create(String serviceName, * the specified name. * * @param serviceName the name of the owning gRPC service - * @param name the method name + * @param name the method name * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static Builder unary(String serviceName, String name) { @@ -126,7 +126,7 @@ public static Builder unary(String serviceName, String name) { * the specified name. * * @param serviceName the name of the owning gRPC service - * @param name the method name + * @param name the method name * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static Builder clientStreaming(String serviceName, String name) { @@ -138,7 +138,7 @@ public static Builder clientStreaming(String serviceName, String name) { * the specified name. * * @param serviceName the name of the owning gRPC service - * @param name the method name + * @param name the method name * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static Builder serverStreaming(String serviceName, String name) { @@ -150,7 +150,7 @@ public static Builder serverStreaming(String serviceName, String name) { * the specified name. * * @param serviceName the name of the owning gRPC service - * @param name the method name + * @param name the method name * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static Builder bidirectional(String serviceName, String name) { @@ -179,8 +179,8 @@ public CallCredentials callCredentials() { * Creates a new {@link GrpcClientMethodDescriptor.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 + * @param name the method name + * @param methodType the gRPC method type * @return a new instance of a {@link GrpcClientMethodDescriptor.Builder} */ public static Builder builder(String serviceName, String name, MethodDescriptor.MethodType methodType) { @@ -204,7 +204,7 @@ public String name() { /** * Returns the {@link io.grpc.MethodDescriptor} of this method. * - * @param the request type + * @param the request type * @param the response type * @return The {@link io.grpc.MethodDescriptor} of this method. */ @@ -241,7 +241,7 @@ public interface Rules { * * @param type The type of parameter of this method. * @return this {@link GrpcClientMethodDescriptor.Rules} instance for - * fluent call chaining + * fluent call chaining */ Rules requestType(Class type); @@ -250,7 +250,7 @@ public interface Rules { * * @param type The type of parameter of this method. * @return this {@link GrpcClientMethodDescriptor.Rules} instance for - * fluent call chaining + * fluent call chaining */ Rules responseType(Class type); @@ -280,7 +280,7 @@ public interface Rules { * * @param marshallerSupplier the {@link MarshallerSupplier} for the service * @return this {@link GrpcClientMethodDescriptor.Rules} instance - * for fluent call chaining + * for fluent call chaining */ Rules marshallerSupplier(MarshallerSupplier marshallerSupplier); @@ -291,16 +291,16 @@ public interface Rules { * * @param callCredentials the {@link io.grpc.CallCredentials} to set. * @return this {@link GrpcClientMethodDescriptor.Rules} instance - * for fluent call chaining + * for fluent call chaining */ Rules callCredentials(CallCredentials callCredentials); /** * Set the {@link MethodHandler} that can be used to invoke the method. * - * @param methodHandler the {2link MethodHandler} to use + * @param methodHandler the {2link MethodHandler} to use * @return this {@link GrpcClientMethodDescriptor.Rules} instance - * for fluent call chaining + * for fluent call chaining */ Rules methodHandler(MethodHandler methodHandler); } @@ -325,8 +325,8 @@ public static class Builder * 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 + * @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; @@ -415,10 +415,10 @@ public GrpcClientMethodDescriptor build() { } return new GrpcClientMethodDescriptor(name, - descriptor.build(), - interceptors, - callCredentials, - methodHandler); + descriptor.build(), + interceptors, + callCredentials, + methodHandler); } } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java index 0da4f3171f3..3784f9ed92a 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientStream.java @@ -27,11 +27,11 @@ class GrpcClientStream extends Http2ClientStream { GrpcClientStream(Http2ClientConnection connection, - Http2Settings serverSettings, - SocketContext ctx, - Http2StreamConfig http2StreamConfig, - Http2ClientConfig http2ClientConfig, - LockingStreamIdSequence streamIdSeq) { + Http2Settings serverSettings, + SocketContext ctx, + Http2StreamConfig http2StreamConfig, + Http2ClientConfig http2ClientConfig, + LockingStreamIdSequence streamIdSeq) { super(connection, serverSettings, ctx, http2StreamConfig, http2ClientConfig, streamIdSeq); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java index 43b10f17f40..337bfeaa710 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcProtocolProvider.java @@ -49,9 +49,9 @@ public GrpcClientProtocolConfig defaultConfig() { @Override public GrpcClient protocol(WebClient client, GrpcClientProtocolConfig config) { return new GrpcClientImpl(client, - GrpcClientConfig.builder() - .from(client.prototype()) - .protocolConfig(config) - .buildPrototype()); + GrpcClientConfig.builder() + .from(client.prototype()) + .protocolConfig(config) + .buildPrototype()); } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java index 4af0bede8f6..831e2a21095 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcServiceClient.java @@ -39,9 +39,9 @@ public interface GrpcServiceClient { * * @param methodName method name * @param request the request - * @return the response * @param type of request * @param type of response + * @return the response */ ResT unary(String methodName, ReqT request); @@ -61,9 +61,9 @@ public interface GrpcServiceClient { * * @param methodName method name * @param request the request - * @return the response iterator * @param type of request * @param type of response + * @return the response iterator */ Iterator serverStream(String methodName, ReqT request); @@ -83,9 +83,9 @@ public interface GrpcServiceClient { * * @param methodName method name * @param request the request iterator - * @return the response * @param type of request * @param type of response + * @return the response */ ResT clientStream(String methodName, Iterator request); @@ -94,9 +94,9 @@ public interface GrpcServiceClient { * * @param methodName method name * @param response the response observer - * @return the request observer * @param type of request * @param type of response + * @return the request observer */ StreamObserver clientStream(String methodName, StreamObserver response); @@ -105,9 +105,9 @@ public interface GrpcServiceClient { * * @param methodName method name * @param request request iterator - * @return response iterator * @param type of request * @param type of response + * @return response iterator */ Iterator bidi(String methodName, Iterator request); @@ -116,9 +116,9 @@ public interface GrpcServiceClient { * * @param methodName method name * @param response the response observer - * @return the request observer * @param type of request * @param type of response + * @return the request observer */ StreamObserver bidi(String methodName, StreamObserver response); } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index d46da45be2b..2ce34db5d5b 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -42,8 +42,9 @@ class GrpcUnaryClientCall extends GrpcBaseClientCall { private volatile boolean requestSent; private volatile boolean responseSent; - GrpcUnaryClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, - CallOptions callOptions) { + GrpcUnaryClientCall(GrpcClientImpl grpcClient, + MethodDescriptor methodDescriptor, + CallOptions callOptions) { super(grpcClient, methodDescriptor, callOptions); }