All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.inverno.mod.grpc.server.internal.GenericGrpcServer Maven / Gradle / Ivy

There is a newer version: 1.12.0
Show newest version
/*
 * 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(); } }