io.quarkus.grpc.runtime.supports.Channels Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of quarkus-grpc Show documentation
Show all versions of quarkus-grpc Show documentation
Serve and consume gRPC services
package io.quarkus.grpc.runtime.supports;
import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
import static io.grpc.netty.NettyChannelBuilder.DEFAULT_FLOW_CONTROL_WINDOW;
import static io.quarkus.grpc.runtime.config.GrpcClientConfiguration.DNS;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import jakarta.enterprise.context.spi.CreationalContext;
import org.eclipse.microprofile.context.ManagedExecutor;
import org.jboss.logging.Logger;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.MethodDescriptor;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.BeanDestroyer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.grpc.GrpcClient;
import io.quarkus.grpc.RegisterClientInterceptor;
import io.quarkus.grpc.runtime.ClientInterceptorStorage;
import io.quarkus.grpc.runtime.GrpcClientInterceptorContainer;
import io.quarkus.grpc.runtime.config.GrpcClientConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerConfiguration;
import io.quarkus.grpc.runtime.config.SslClientConfig;
import io.quarkus.grpc.runtime.stork.StorkGrpcChannel;
import io.quarkus.grpc.runtime.stork.StorkMeasuringGrpcInterceptor;
import io.quarkus.grpc.runtime.stork.VertxStorkMeasuringGrpcInterceptor;
import io.quarkus.grpc.spi.GrpcBuilderProvider;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.util.ClassPathUtils;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import io.smallrye.stork.Stork;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.core.net.SocketAddress;
import io.vertx.grpc.client.GrpcClientChannel;
@SuppressWarnings({ "OptionalIsPresent" })
public class Channels {
private static final Logger LOGGER = Logger.getLogger(Channels.class.getName());
private Channels() {
// Avoid direct instantiation
}
@SuppressWarnings("rawtypes")
public static Channel createChannel(String name, Set perClientInterceptors) throws Exception {
ArcContainer container = Arc.container();
InstanceHandle instance = container.instance(GrpcClientConfigProvider.class);
if (!instance.isAvailable()) {
throw new IllegalStateException("Unable to find the GrpcClientConfigProvider");
}
GrpcClientConfigProvider configProvider = instance.get();
GrpcClientConfiguration config = configProvider.getConfiguration(name);
if (config == null && LaunchMode.current() == LaunchMode.TEST) {
LOGGER.infof(
"gRPC client %s created without configuration. We are assuming that it's created to test your gRPC services.",
name);
config = testConfig(configProvider.getServerConfiguration());
}
if (config == null) {
throw new IllegalStateException("gRPC client " + name + " is missing configuration.");
}
GrpcBuilderProvider provider = GrpcBuilderProvider.findChannelBuilderProvider(config);
boolean vertxGrpc = config.useQuarkusGrpcClient;
String host = config.host;
int port = config.port;
String nameResolver = config.nameResolver;
boolean stork = Stork.STORK.equalsIgnoreCase(nameResolver);
String[] resolverSplit = nameResolver.split(":");
String resolver = provider != null ? provider.resolver() : resolverSplit[0];
// TODO -- does this work for Vert.x gRPC client?
if (provider != null) {
host = provider.adjustHost(host);
} else if (!vertxGrpc && DNS.equalsIgnoreCase(resolver)) {
host = "/" + host; // dns or xds name resolver needs triple slash at the beginning
}
// Client-side interceptors
GrpcClientInterceptorContainer interceptorContainer = container
.instance(GrpcClientInterceptorContainer.class).get();
if (stork) {
perClientInterceptors = new HashSet<>(perClientInterceptors);
if (vertxGrpc) {
perClientInterceptors.add(VertxStorkMeasuringGrpcInterceptor.class.getName());
} else {
perClientInterceptors.add(StorkMeasuringGrpcInterceptor.class.getName());
}
}
boolean plainText = config.ssl.trustStore.isEmpty();
Optional usePlainText = config.plainText;
if (usePlainText.isPresent()) {
plainText = usePlainText.get();
}
if (!vertxGrpc) {
String target = String.format("%s://%s:%d", resolver, host, port);
LOGGER.debugf("Target for client '%s': %s", name, target);
SslContext context = null;
if (!plainText && provider == null) {
Path trustStorePath = config.ssl.trustStore.orElse(null);
Path certificatePath = config.ssl.certificate.orElse(null);
Path keyPath = config.ssl.key.orElse(null);
SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient();
if (trustStorePath != null) {
try (InputStream stream = streamFor(trustStorePath, "trust store")) {
sslContextBuilder.trustManager(stream);
} catch (IOException e) {
throw new UncheckedIOException("Configuring gRPC client trust store failed", e);
}
}
if (certificatePath != null && keyPath != null) {
try (InputStream certificate = streamFor(certificatePath, "certificate");
InputStream key = streamFor(keyPath, "key")) {
sslContextBuilder.keyManager(certificate, key);
} catch (IOException e) {
throw new UncheckedIOException("Configuring gRPC client certificate failed", e);
}
}
context = sslContextBuilder.build();
}
String loadBalancingPolicy = stork ? Stork.STORK : config.loadBalancingPolicy;
ManagedChannelBuilder> builder;
if (provider != null) {
builder = provider.createChannelBuilder(config, target);
} else {
builder = NettyChannelBuilder
.forTarget(target)
// clients are intercepted using the IOThreadClientInterceptor interceptor which will decide on which
// thread the messages should be processed.
.directExecutor() // will use I/O thread - must not be blocked.
.offloadExecutor(Infrastructure.getDefaultExecutor())
.defaultLoadBalancingPolicy(loadBalancingPolicy)
.flowControlWindow(config.flowControlWindow.orElse(DEFAULT_FLOW_CONTROL_WINDOW))
.keepAliveWithoutCalls(config.keepAliveWithoutCalls)
.maxHedgedAttempts(config.maxHedgedAttempts)
.maxRetryAttempts(config.maxRetryAttempts)
.maxInboundMetadataSize(config.maxInboundMetadataSize.orElse(DEFAULT_MAX_HEADER_LIST_SIZE))
.maxInboundMessageSize(config.maxInboundMessageSize.orElse(DEFAULT_MAX_MESSAGE_SIZE))
.negotiationType(NegotiationType.valueOf(config.negotiationType.toUpperCase()));
}
if (config.retry) {
builder.enableRetry();
} else {
builder.disableRetry();
}
if (config.maxTraceEvents.isPresent()) {
builder.maxTraceEvents(config.maxTraceEvents.getAsInt());
}
Optional userAgent = config.userAgent;
if (userAgent.isPresent()) {
builder.userAgent(userAgent.get());
}
if (config.retryBufferSize.isPresent()) {
builder.retryBufferSize(config.retryBufferSize.getAsLong());
}
if (config.perRpcBufferLimit.isPresent()) {
builder.perRpcBufferLimit(config.perRpcBufferLimit.getAsLong());
}
Optional overrideAuthority = config.overrideAuthority;
if (overrideAuthority.isPresent()) {
builder.overrideAuthority(overrideAuthority.get());
}
Optional keepAliveTime = config.keepAliveTime;
if (keepAliveTime.isPresent()) {
builder.keepAliveTime(keepAliveTime.get().toMillis(), TimeUnit.MILLISECONDS);
}
Optional keepAliveTimeout = config.keepAliveTimeout;
if (keepAliveTimeout.isPresent()) {
builder.keepAliveTimeout(keepAliveTimeout.get().toMillis(), TimeUnit.MILLISECONDS);
}
Optional idleTimeout = config.idleTimeout;
if (idleTimeout.isPresent()) {
builder.idleTimeout(idleTimeout.get().toMillis(), TimeUnit.MILLISECONDS);
}
if (plainText && provider == null) {
builder.usePlaintext();
}
if (context != null && (builder instanceof NettyChannelBuilder)) {
NettyChannelBuilder ncBuilder = (NettyChannelBuilder) builder;
ncBuilder.sslContext(context);
}
interceptorContainer.getSortedPerServiceInterceptors(perClientInterceptors).forEach(builder::intercept);
interceptorContainer.getSortedGlobalInterceptors().forEach(builder::intercept);
LOGGER.info(String.format("Creating %s gRPC channel ...",
provider != null ? provider.channelInfo(config) : "Netty"));
return builder.build();
} else {
HttpClientOptions options = new HttpClientOptions(); // TODO options
options.setHttp2ClearTextUpgrade(false); // this fixes i30379
if (!plainText) {
if (config.ssl.trustStore.isPresent()) {
Optional trustStorePath = config.ssl.trustStore;
if (trustStorePath.isPresent()) {
PemTrustOptions to = new PemTrustOptions();
to.addCertValue(bufferFor(trustStorePath.get(), "trust store"));
options.setTrustOptions(to);
options.setSsl(true);
options.setUseAlpn(true);
}
Optional certificatePath = config.ssl.certificate;
Optional keyPath = config.ssl.key;
if (certificatePath.isPresent() && keyPath.isPresent()) {
PemKeyCertOptions cko = new PemKeyCertOptions();
cko.setCertValue(bufferFor(certificatePath.get(), "certificate"));
cko.setKeyValue(bufferFor(keyPath.get(), "key"));
options.setKeyCertOptions(cko);
options.setSsl(true);
options.setUseAlpn(true);
}
}
}
options.setKeepAlive(config.keepAliveWithoutCalls);
Optional keepAliveTimeout = config.keepAliveTimeout;
if (keepAliveTimeout.isPresent()) {
int keepAliveTimeoutN = (int) keepAliveTimeout.get().toSeconds();
options.setKeepAliveTimeout(keepAliveTimeoutN);
options.setHttp2KeepAliveTimeout(keepAliveTimeoutN);
}
Optional idleTimeout = config.idleTimeout;
if (idleTimeout.isPresent()) {
options.setIdleTimeout((int) idleTimeout.get().toMillis());
options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS);
}
// Use the convention defined by Quarkus Micrometer Vert.x metrics to create metrics prefixed with grpc..
// See io.quarkus.micrometer.runtime.binder.vertx.VertxMeterBinderAdapter.extractPrefix and
// io.quarkus.micrometer.runtime.binder.vertx.VertxMeterBinderAdapter.extractClientName
options.setMetricsName("grpc|" + name);
Vertx vertx = container.instance(Vertx.class).get();
io.vertx.grpc.client.GrpcClient client = io.vertx.grpc.client.GrpcClient.client(vertx, options);
Channel channel;
if (stork) {
ManagedExecutor executor = container.instance(ManagedExecutor.class).get();
channel = new StorkGrpcChannel(client, config.host, config.stork, executor); // host = service-name
} else {
channel = new GrpcClientChannel(client, SocketAddress.inetSocketAddress(port, host));
}
LOGGER.debugf("Target for client '%s': %s", name, host + ":" + port);
List interceptors = new ArrayList<>();
interceptors.addAll(interceptorContainer.getSortedPerServiceInterceptors(perClientInterceptors));
interceptors.addAll(interceptorContainer.getSortedGlobalInterceptors());
LOGGER.info("Creating Vert.x gRPC channel ...");
return new InternalGrpcChannel(client, channel, ClientInterceptors.intercept(channel, interceptors));
}
}
private static GrpcClientConfiguration testConfig(GrpcServerConfiguration serverConfiguration) {
GrpcClientConfiguration config = new GrpcClientConfiguration();
config.port = serverConfiguration.testPort;
config.host = serverConfiguration.host;
config.plainText = Optional.of(serverConfiguration.plainText);
config.compression = Optional.empty();
config.flowControlWindow = OptionalInt.empty();
config.idleTimeout = Optional.empty();
config.keepAliveTime = Optional.empty();
config.keepAliveTimeout = Optional.empty();
config.loadBalancingPolicy = "pick_first";
config.maxHedgedAttempts = 5;
config.maxInboundMessageSize = OptionalInt.empty();
config.maxInboundMetadataSize = OptionalInt.empty();
config.maxRetryAttempts = 0;
config.maxTraceEvents = OptionalInt.empty();
config.nameResolver = DNS;
config.negotiationType = "PLAINTEXT";
config.overrideAuthority = Optional.empty();
config.perRpcBufferLimit = OptionalLong.empty();
config.retry = false;
config.retryBufferSize = OptionalLong.empty();
config.ssl = new SslClientConfig();
config.ssl.key = Optional.empty();
config.ssl.certificate = Optional.empty();
config.ssl.trustStore = Optional.empty();
config.userAgent = Optional.empty();
if (serverConfiguration.ssl.certificate.isPresent() || serverConfiguration.ssl.keyStore.isPresent()) {
LOGGER.warn("gRPC client created without configuration and the gRPC server is configured for SSL. " +
"Configuring SSL for such clients is not supported.");
}
return config;
}
private static Buffer bufferFor(Path path, String resourceName) throws IOException {
try (InputStream stream = streamFor(path, resourceName)) {
return Buffer.buffer(stream.readAllBytes());
}
}
private static InputStream streamFor(Path path, String resourceName) {
final InputStream resource = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(ClassPathUtils.toResourceName(path));
if (resource != null) {
return resource;
} else {
try {
return Files.newInputStream(path);
} catch (IOException e) {
throw new UncheckedIOException("Unable to read " + resourceName + " from " + path, e);
}
}
}
@SuppressWarnings("unchecked")
public static Channel retrieveChannel(String name, Set perClientInterceptors) {
ClientInterceptorStorage clientInterceptorStorage = Arc.container().instance(ClientInterceptorStorage.class).get();
Annotation[] qualifiers = new Annotation[perClientInterceptors.size() + 1];
int idx = 0;
qualifiers[idx++] = GrpcClient.Literal.of(name);
for (String interceptor : perClientInterceptors) {
qualifiers[idx++] = RegisterClientInterceptor.Literal
.of((Class extends ClientInterceptor>) clientInterceptorStorage.getPerClientInterceptor(interceptor));
}
InstanceHandle instance = Arc.container().instance(Channel.class, qualifiers);
if (!instance.isAvailable()) {
throw new IllegalStateException("Unable to retrieve the gRPC Channel " + name);
}
return instance.get();
}
public static class ChannelDestroyer implements BeanDestroyer {
@Override
public void destroy(Channel instance, CreationalContext creationalContext, Map params) {
if (instance instanceof ManagedChannel) {
ManagedChannel channel = (ManagedChannel) instance;
LOGGER.info("Shutting down gRPC channel " + channel);
channel.shutdownNow();
try {
channel.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOGGER.info("Unable to shutdown channel after 10 seconds");
Thread.currentThread().interrupt();
}
} else if (instance instanceof InternalGrpcChannel) {
InternalGrpcChannel channel = (InternalGrpcChannel) instance;
Channel original = channel.original;
LOGGER.info("Shutting down Vert.x gRPC channel " + original);
try {
if (original instanceof StorkGrpcChannel) {
((StorkGrpcChannel) original).close();
}
channel.client.close().toCompletionStage().toCompletableFuture().get(10, TimeUnit.SECONDS);
} catch (ExecutionException | TimeoutException e) {
LOGGER.warn("Unable to shutdown channel after 10 seconds", e);
} catch (InterruptedException e) {
LOGGER.info("Unable to shutdown channel after 10 seconds");
Thread.currentThread().interrupt();
}
}
}
}
private static class InternalGrpcChannel extends Channel {
private final io.vertx.grpc.client.GrpcClient client;
private final Channel original;
private final Channel delegate;
public InternalGrpcChannel(io.vertx.grpc.client.GrpcClient client, Channel original, Channel delegate) {
this.client = client;
this.original = original;
this.delegate = delegate;
}
@Override
public ClientCall newCall(
MethodDescriptor methodDescriptor, CallOptions callOptions) {
return delegate.newCall(methodDescriptor, callOptions);
}
@Override
public String authority() {
return delegate.authority();
}
}
}