
io.inverno.mod.grpc.server.internal.GenericGrpcServer Maven / Gradle / Ivy
/*
* Copyright 2024 Jeremy Kuhn
*
* 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.inverno.mod.grpc.server.internal;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.Message;
import io.inverno.core.annotation.Bean;
import io.inverno.mod.base.net.NetService;
import io.inverno.mod.grpc.base.GrpcException;
import io.inverno.mod.grpc.base.GrpcHeaders;
import io.inverno.mod.grpc.base.GrpcMessageCompressor;
import io.inverno.mod.grpc.base.GrpcMessageCompressorService;
import io.inverno.mod.grpc.base.GrpcStatus;
import io.inverno.mod.grpc.base.internal.GrpcMessageReader;
import io.inverno.mod.grpc.base.internal.GrpcMessageWriter;
import io.inverno.mod.grpc.base.internal.IdentityGrpcMessageCompressor;
import io.inverno.mod.grpc.server.GrpcExchange;
import io.inverno.mod.grpc.server.GrpcExchangeHandler;
import io.inverno.mod.grpc.server.GrpcRequest;
import io.inverno.mod.grpc.server.GrpcResponse;
import io.inverno.mod.grpc.server.GrpcServer;
import io.inverno.mod.http.base.ExchangeContext;
import io.inverno.mod.http.base.HttpException;
import io.inverno.mod.http.base.Status;
import static io.inverno.mod.http.base.Status.BAD_GATEWAY;
import static io.inverno.mod.http.base.Status.BAD_REQUEST;
import static io.inverno.mod.http.base.Status.FORBIDDEN;
import static io.inverno.mod.http.base.Status.GATEWAY_TIMEOUT;
import static io.inverno.mod.http.base.Status.NOT_FOUND;
import static io.inverno.mod.http.base.Status.SERVICE_UNAVAILABLE;
import static io.inverno.mod.http.base.Status.TOO_MANY_REQUESTS;
import static io.inverno.mod.http.base.Status.UNAUTHORIZED;
import io.inverno.mod.http.server.ErrorExchange;
import io.inverno.mod.http.server.Exchange;
import io.inverno.mod.http.server.ExchangeHandler;
import io.netty.handler.codec.http2.Http2Error;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
* Generic {@link GrpcServer} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.9
*/
@Bean( name = "grpcServer" )
public class GenericGrpcServer implements GrpcServer {
/**
* The gRPC message compressor service.
*/
private final GrpcMessageCompressorService compressorService;
/**
* The Protocol buffer extension registry.
*/
private final ExtensionRegistry extensionRegistry;
/**
* The net service.
*/
private final NetService netService;
/**
*
* Creates a generic gRPC server.
*
*
* @param compressorService the gRPC message compressor service
* @param extensionRegistry the Protocol buffer extension registry
* @param netService the net service
*/
public GenericGrpcServer(GrpcMessageCompressorService compressorService, ExtensionRegistry extensionRegistry, NetService netService) {
this.compressorService = compressorService;
this.extensionRegistry = extensionRegistry;
this.netService = netService;
}
@Override
public , C extends Message, D extends Message, E extends GrpcExchange.Unary> ExchangeHandler unary(C defaultRequestInstance, D defaultResponseInstance, GrpcExchangeHandler, GrpcResponse.Unary, E> grpcExchangeHandler) {
Function exchangeFactory = exchange -> (E)new GenericGrpcExchange.GenericUnary<>(exchange, () -> this.createRequest(exchange, defaultRequestInstance), () -> this.createResponse(exchange, defaultRequestInstance));
return new GrpcExchangeHandlerAdapter<>(grpcExchangeHandler, exchangeFactory, this::handleError);
}
@Override
public , C extends Message, D extends Message, E extends GrpcExchange.ClientStreaming> ExchangeHandler clientStreaming(C defaultRequestInstance, D defaultResponseInstance, GrpcExchangeHandler, GrpcResponse.Unary, E> grpcExchangeHandler) {
Function exchangeFactory = exchange -> (E)new GenericGrpcExchange.GenericClientStreaming<>(exchange, () -> this.createRequest(exchange, defaultRequestInstance), () -> this.createResponse(exchange, defaultRequestInstance));
return new GrpcExchangeHandlerAdapter<>(grpcExchangeHandler, exchangeFactory, this::handleError);
}
@Override
public , C extends Message, D extends Message, E extends GrpcExchange.ServerStreaming> ExchangeHandler serverStreaming(C defaultRequestInstance, D defaultResponseInstance, GrpcExchangeHandler, GrpcResponse.Streaming, E> grpcExchangeHandler) {
Function exchangeFactory = exchange -> (E)new GenericGrpcExchange.GenericServerStreaming<>(exchange, () -> this.createRequest(exchange, defaultRequestInstance), () -> this.createResponse(exchange, defaultRequestInstance));
return new GrpcExchangeHandlerAdapter<>(grpcExchangeHandler, exchangeFactory, this::handleError);
}
@Override
public , C extends Message, D extends Message, E extends GrpcExchange.BidirectionalStreaming> ExchangeHandler bidirectionalStreaming(C defaultRequestInstance, D defaultResponseInstance, GrpcExchangeHandler, GrpcResponse.Streaming, E> grpcExchangeHandler) {
Function exchangeFactory = exchange -> (E)new GenericGrpcExchange.GenericBidirectionalStreaming<>(exchange, () -> this.createRequest(exchange, defaultRequestInstance), () -> this.createResponse(exchange, defaultRequestInstance));
return new GrpcExchangeHandlerAdapter<>(grpcExchangeHandler, exchangeFactory, this::handleError);
}
@Override
public > ExchangeHandler errorHandler() {
return new GenericGrpcErrorExchangeHandler<>(this);
}
/**
*
* Creates a gRPC server request.
*
*
*
* It determines the compressor to use based on the {@link GrpcHeaders#NAME_GRPC_MESSAGE_ENCODING} header in the request, if none is specified, it falls back to the
* {@link IdentityGrpcMessageCompressor}.
*
*
*
* In case the client sends a request with an unsupported encoding, the {@link GrpcHeaders#NAME_GRPC_ACCEPT_MESSAGE_ENCODING} header is set in the response with the list of supported message
* encodings.
*
*
* @param the exchange context type
* @param the server exchange type
* @param the gRPC request message type
* @param the gRPC request type
* @param exchange the server HTTP exchange
* @param defaultRequestInstance the default request message instance
*
* @return a new gRPC server request
*
* @throws GrpcException if the message encoding specified in the request is not supported
*/
private , C extends Message, D extends GrpcRequest> D createRequest(B exchange, C defaultRequestInstance) throws GrpcException {
GrpcMessageCompressor messageCompressor = exchange.request().headers().get(GrpcHeaders.NAME_GRPC_MESSAGE_ENCODING)
.map(value -> this.compressorService.getMessageCompressor(value).orElseThrow(() -> {
exchange.response().headers(headers -> headers.set(GrpcHeaders.NAME_GRPC_ACCEPT_MESSAGE_ENCODING, this.compressorService.getMessageEncodings().stream().collect(Collectors.joining(","))));
return new GrpcException(GrpcStatus.UNIMPLEMENTED, "Unsupported message encoding: " + value);
}))
.or(() -> this.compressorService.getMessageCompressor(GrpcHeaders.VALUE_IDENTITY))
.orElse(IdentityGrpcMessageCompressor.INSTANCE);
GrpcMessageReader messageReader = new GrpcMessageReader<>(defaultRequestInstance, this.extensionRegistry, this.netService, messageCompressor);
return (D)new GenericGrpcRequest<>(exchange.request(), messageReader, this.extensionRegistry);
}
/**
*
* Creates a gRPC server response.
*
*
* @param the exchange context type
* @param the server exchange type
* @param the gRPC response message type
* @param the gRPC response type
* @param exchange the server HTTP exchange
* @param defaultResponseInstance the default response message instance
*
* @return a new gRPC server response
*/
private , C extends Message, D extends GrpcResponse> D createResponse(B exchange, C defaultResponseInstance) {
return (D)new GenericGrpcResponse<>(exchange.response(), () -> this.createMessageWriter(exchange), this.extensionRegistry, () -> exchange.reset(Http2Error.CANCEL.code()));
}
/**
*
* Creates a gRPC message writer.
*
*
*
* It determines the compressor to use based on the {@link GrpcHeaders#NAME_GRPC_MESSAGE_ENCODING} header in the request, if none is specified it selects the first supported message encoding
* listed in the {@link GrpcHeaders#NAME_GRPC_ACCEPT_MESSAGE_ENCODING} header. If no gRPC message compressor could be resolved it falls back to the
* {@link IdentityGrpcMessageCompressor}.
*
*
* @param the exchange context type
* @param the server exchange type
* @param the response message type
* @param exchange the server HTTP exchange
*
* @return a gRPC message writer
*
* throws GrpcException if the message encoding specified by the client is not supported
*/
private , C extends Message> GrpcMessageWriter createMessageWriter(B exchange) throws GrpcException {
// if message encoding has been specified in the response headers we must throw an error if this is not supported
GrpcMessageCompressor messageCompressor = exchange.response().headers().get(GrpcHeaders.NAME_GRPC_MESSAGE_ENCODING)
.map(value -> this.compressorService.getMessageCompressor(value).orElseThrow(() -> {
exchange.response().headers(headers -> headers.set(GrpcHeaders.NAME_GRPC_ACCEPT_MESSAGE_ENCODING, this.compressorService.getMessageEncodings().stream().collect(Collectors.joining(","))));
return new GrpcException(GrpcStatus.UNIMPLEMENTED, "Unsupported message encoding: " + value);
}))
.orElseGet(() -> {
GrpcMessageCompressor c = exchange.request().headers().get(GrpcHeaders.NAME_GRPC_ACCEPT_MESSAGE_ENCODING)
.flatMap(value -> this.compressorService.getMessageCompressor(value.split(",")))
.or(() -> this.compressorService.getMessageCompressor(GrpcHeaders.VALUE_IDENTITY))
.orElse(IdentityGrpcMessageCompressor.INSTANCE);
exchange.response().headers(headers -> headers.set(GrpcHeaders.NAME_GRPC_MESSAGE_ENCODING, c.getMessageEncoding()));
return c;
});
return new GrpcMessageWriter<>(this.netService, messageCompressor);
}
/**
*
* Handles the specified error.
*
*
*
* This basically map the error to a gRPC status and message and set them in the HTTP response trailers and then terminates the exchange.
*
*
*
* This is invoked by the {@link GrpcServer#errorHandler()} and by the {@link GrpcExchangeHandlerAdapter}.
*
*
* @param the exchange context type
* @param the server exchange type
* @param exchange the server HTTP exchange
* @param error the error
*/
public > void handleError(B exchange, Throwable error) {
GenericGrpcExchange.LOGGER.error("gRPC exchange processing error", error);
exchange.response()
.headers(headers -> headers.status(Status.OK)) // just make sure we have 200... https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
.trailers(trailers -> {
GrpcStatus grpcStatus;
if(error instanceof GrpcException) {
GrpcException grpcError = (GrpcException)error;
grpcStatus = grpcError.getStatus();
}
else if(error instanceof HttpException) {
// This is not supposed to happen but we never know
// https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
HttpException httpError = (HttpException)error;
switch(httpError.getStatus()) {
case BAD_REQUEST: grpcStatus = GrpcStatus.INTERNAL;
break;
case UNAUTHORIZED: grpcStatus = GrpcStatus.UNAUTHENTICATED;
break;
case FORBIDDEN: grpcStatus = GrpcStatus.PERMISSION_DENIED;
break;
case NOT_FOUND: grpcStatus = GrpcStatus.UNIMPLEMENTED;
break;
case TOO_MANY_REQUESTS:
case BAD_GATEWAY:
case SERVICE_UNAVAILABLE:
case GATEWAY_TIMEOUT: grpcStatus = GrpcStatus.UNAVAILABLE;
break;
default: grpcStatus = GrpcStatus.UNKNOWN;
}
}
else if(error instanceof IllegalArgumentException) {
grpcStatus = GrpcStatus.INVALID_ARGUMENT;
}
else {
grpcStatus = GrpcStatus.UNKNOWN;
}
trailers.set(GrpcHeaders.NAME_GRPC_STATUS, Integer.toString(grpcStatus.getCode()));
if(error.getMessage() != null) {
trailers.set(GrpcHeaders.NAME_GRPC_STATUS_MESSAGE, error.getMessage());
}
})
.body().empty();
}
}