Skip to content

Commit

Permalink
Introduces a new interface implemented by all gRPC client proxies tha…
Browse files Browse the repository at this point in the history
…t can be used to update the server's listening port dynamically. This is very useful during testing. Removes reflective code from the client gRPC library and updates tests.
  • Loading branch information
spericas committed Aug 1, 2024
1 parent 4450e1f commit 0d2e18c
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
import java.util.stream.Stream;

import io.helidon.grpc.api.Grpc;
import io.helidon.microprofile.grpc.client.GrpcConfigurablePort;
import io.helidon.microprofile.testing.junit5.HelidonTest;

import io.grpc.stub.StreamObserver;
import jakarta.inject.Inject;
import jakarta.ws.rs.client.WebTarget;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.is;
Expand All @@ -36,10 +39,20 @@
@HelidonTest
class StringServiceTest {

@Inject
private WebTarget webTarget;

@Inject
@Grpc.GrpcProxy
private StringServiceClient client;

@BeforeEach
void updatePort() {
if (client instanceof GrpcConfigurablePort c) {
c.channelPort(webTarget.getUri().getPort());
}
}

@Test
void testUnaryUpper() {
Strings.StringMessage res = client.upper(newMessage("hello"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package io.helidon.microprofile.grpc.client;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -25,8 +24,6 @@
import io.helidon.webclient.grpc.GrpcClient;

import io.grpc.Channel;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.Extension;

/**
* GrpcChannelsProvider is a factory for pre-configured gRPC Channel instances.
Expand All @@ -43,11 +40,6 @@ public class GrpcChannelsProvider {
*/
public static final String DEFAULT_HOST = "localhost";

/**
* The configuration key for the channels' configuration.
*/
public static final String CFG_KEY_CHANNELS = "channels";

/**
* A constant for holding the default port (which is "1408").
*/
Expand Down Expand Up @@ -136,37 +128,13 @@ private Channel createChannel(String name, GrpcChannelDescriptor descriptor) {
throw new IllegalArgumentException("Client TLS must be configured for gRPC proxy client");
}
int port = descriptor.port();
if (port <= 0) {
port = discoverServerPort(name);
}
GrpcClient grpcClient = GrpcClient.builder()
.tls(clientTls)
.baseUri("https://" + descriptor.host() + ":" + port)
.build();
return grpcClient.channel();
}

/**
* Used for unit testing: if port not set, try to obtain port from server CDI
* extension via reflection, without introducing a static dependency.
*
* @param name the channel name
* @return server port
* @throws java.lang.IllegalArgumentException if unable to discover port
*/
@SuppressWarnings("unchecked")
private static int discoverServerPort(String name) {
try {
Class<? extends Extension> extClass = (Class<? extends Extension>) Class
.forName("io.helidon.microprofile.server.ServerCdiExtension");
Extension extension = CDI.current().getBeanManager().getExtension(extClass);
Method m = extension.getClass().getMethod("port", String.class);
return (int) m.invoke(extension, new Object[] {"@default"});
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Unable to determine server port for channel " + name);
}
}

/**
* Builder builds an instance of {@link GrpcChannelsProvider}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import static java.lang.System.Logger.Level;

/**
* A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor.Builder} instances
* A builder for constructing a {@link io.helidon.microprofile.grpc.client.ClientServiceDescriptor} instances
* from an annotated POJO.
*/
class GrpcClientBuilder extends AbstractServiceBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.microprofile.grpc.client;

/**
* Interface implemented by all gRPC client proxies. The method {@link #channelPort} can be
* called at runtime to override the client URI port from config. Typically used for testing.
*/
public interface GrpcConfigurablePort {

/**
* Name of single setter method on this interface.
*/
String CHANNEL_PORT = "channelPort";

/**
* Overrides client URI port.
*
* @param value the new port value
*/
void channelPort(int value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static <T> GrpcProxyBuilder<T> create(Channel channel, Class<T> type) {
*/
@Override
public T build() {
return client.proxy(type);
return client.proxy(type, GrpcConfigurablePort.class);
}

private static ClientServiceDescriptor createDescriptor(Class<?> type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.helidon.grpc.core.InterceptorWeights;
import io.helidon.grpc.core.MethodHandler;
import io.helidon.grpc.core.WeightedBag;
import io.helidon.webclient.api.ClientUri;

import io.grpc.CallOptions;
import io.grpc.Channel;
Expand All @@ -45,6 +46,8 @@
*/
public class GrpcServiceClient {

private final Channel channel;

private final HashMap<String, GrpcMethodStub<?, ?>> methodStubs;

private final ClientServiceDescriptor clientServiceDescriptor;
Expand Down Expand Up @@ -76,6 +79,7 @@ public static GrpcServiceClient create(Channel channel, ClientServiceDescriptor
private GrpcServiceClient(Channel channel,
CallOptions callOptions,
ClientServiceDescriptor clientServiceDescriptor) {
this.channel = channel;
this.clientServiceDescriptor = clientServiceDescriptor;
this.methodStubs = new HashMap<>();

Expand Down Expand Up @@ -137,6 +141,13 @@ public String serviceName() {
Object invoke(String name, Object[] args) {
GrpcMethodStub<?, ?> stub = methodStubs.get(name);
if (stub == null) {
// may be used during testing to override a channel's port
if (name.equals(GrpcConfigurablePort.CHANNEL_PORT)) {
io.helidon.webclient.grpc.GrpcChannel grpcChannel = (io.helidon.webclient.grpc.GrpcChannel) channel;
ClientUri uri = grpcChannel.grpcClient().clientConfig().baseUri().orElseThrow();
uri.port((int) args[0]);
return null;
}
throw Status.INTERNAL.withDescription("gRPC method '" + name + "' does not exist").asRuntimeException();
}
ClientMethodDescriptor descriptor = stub.descriptor();
Expand Down Expand Up @@ -164,6 +175,7 @@ Object invoke(String name, Object[] args) {
@SuppressWarnings("unchecked")
public <T> T proxy(Class<T> type, Class<?>... extraTypes) {
Map<String, String> names = new HashMap<>();
names.put(GrpcConfigurablePort.CHANNEL_PORT, GrpcConfigurablePort.CHANNEL_PORT); // for testing
for (ClientMethodDescriptor methodDescriptor : clientServiceDescriptor.methods()) {
MethodHandler<?, ?> methodHandler = methodDescriptor.methodHandler();
if (methodHandler != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.grpc.stub.StreamObserver;
import jakarta.inject.Inject;
import jakarta.ws.rs.client.WebTarget;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static io.helidon.grpc.core.ResponseHelper.complete;
Expand All @@ -53,6 +54,13 @@ class EchoServiceTest {
@Grpc.GrpcProxy
private EchoServiceClient proxyClient;

@BeforeEach
void updatePort() {
if (proxyClient instanceof GrpcConfigurablePort client) {
client.channelPort(webTarget.getUri().getPort());
}
}

@Test
void testEcho() throws InterruptedException, ExecutionException, TimeoutException {
Tls clientTls = Tls.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
/**
* Helidon's implementation of a gRPC {@link Channel}.
*/
class GrpcChannel extends Channel {
public class GrpcChannel extends Channel {

private final GrpcClientImpl grpcClient;

Expand All @@ -39,6 +39,15 @@ class GrpcChannel extends Channel {
this.grpcClient = (GrpcClientImpl) grpcClient;
}

/**
* Underlying gRPC Client for this channel.
*
* @return the gRPC client
*/
public GrpcClient grpcClient() {
return grpcClient;
}

@Override
public <ReqT, ResT> ClientCall<ReqT, ResT> newCall(
MethodDescriptor<ReqT, ResT> methodDescriptor, CallOptions callOptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,11 @@ static GrpcClient create() {
default Channel channel(Collection<ClientInterceptor> interceptors) {
return channel(interceptors.toArray(new ClientInterceptor[]{}));
}

/**
* Configuration for this gRPC client.
*
* @return the configuration
*/
GrpcClientConfig clientConfig();
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ public Channel channel() {
public Channel channel(ClientInterceptor... interceptors) {
return ClientInterceptors.intercept(channel(), interceptors);
}

@Override
public GrpcClientConfig clientConfig() {
return clientConfig;
}
}

0 comments on commit 0d2e18c

Please sign in to comment.