gRPC

Motivation

A design philosophy for ServiceTalk is cross protocol API symmetry which means that all protocols supported by ServiceTalk should have same constructs and follow the same design principles. We acknowledge that grpc-java implements the gRPC wire protocol for the JVM and is used extensively. However, our design philosophies are different than grpc-java and ServiceTalk also provides HTTP/2 directly for users. The grpc-java team and the ServiceTalk team collaborated to bring HTTP/2 support to Netty, and both share the same underlying protocol implementation. Once you have HTTP/2 support the investment to support the gRPC wire protocol is relatively small but it enables ServiceTalk users to benefit from our design philosophy consistently across all protocols.

Overview

gRPC support in ServiceTalk implements the gRPC wire protocol and provides ServiceTalk APIs for that protocol. It provides all the different Programming Paradigms for client and server. Here is a quick start example of the blocking and aggregated paradigm:

Blocking Client

try (BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080)
        .buildBlocking(new ClientFactory())) {
    HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("Foo").build());
    // use the response
}

Blocking Server

GrpcServers.forPort(8080)
        .listenAndAwait(new ServiceFactory((BlockingGreeterService) (ctx, request) ->
                HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()))
        .awaitShutdown();

Inbound message size limit

To bound memory use, ServiceTalk caps a single decoded inbound gRPC message at 4 MiB by default (matching grpc-java). A message whose declared length exceeds the limit is rejected with RESOURCE_EXHAUSTED before its payload is buffered. The limit applies on the receiving side of every paradigm: a server bounds request messages, a client bounds response messages. For a compressed message it bounds the on-wire (still-compressed) length before buffering, and the decoded length after decompression.

Configure it with GrpcServerBuilder#maxInboundMessageSize(int) / GrpcClientBuilder#maxInboundMessageSize(int): 0 disables the limit and a positive value enforces it (negative values are rejected). The default can be changed globally with the temporary io.servicetalk.grpc.netty.temporaryDefaultMaxInboundMessageSize system property (to be removed in a future release); an explicit builder call takes precedence. Setting that property to -1 enables a warn-only rollout mode globally: an oversized message is delivered but a warning is logged (rate-limited to once every five minutes per client/server) instead of being rejected.

maxInboundMessageSize does not bound the memory used while decompressing a compressed message. That is governed independently by the codec’s own decompressed-bytes cap (ZipCompressionBuilder#maxDecompressedBytes(long), 64 MiB by default), which fails fast before the decoded message is checked against maxInboundMessageSize.

Extensibility and Filters

The design of this protocol involves configuring builders for core protocol concerns, and then appending Filters for extensibility. Filters are described in more detail below but in general they facilitate user code to filter/intercept/modify the request/response processing. Filters can be used for cross-cutting concerns such as authentication, authorization, logging, metrics, tracing, etc…​

Server

The server side is built around the concept of Service. A Service is where your business logic lives. Interface for user service is generated from a provided protocol buffers service definition. Users implement this interface and provide it to the ServiceTalk gRPC server. ServiceTalk internally uses the existing HTTP module as the transport for gRPC. The flow of data from the socket to the gRPC Service is visualized as follows:

+--------+ request  +---------+       +----------+ request  +---------+       +----------+
|        |--------->|  HTTP   |------>|  HTTP    |--------->|  gRPC   |------>|  gRPC    |
| Socket |          | Decoder |       | Service  |          | Decoder |       | Service  |
|        |<---------| Encoder |<------|(for gRPC)|<---------| Encoder |<------|          |
+--------+ response +---------+       +----------+ response +---------+       +----------+

Each Service has access to a GrpcServiceContext which provides additional context (via ConnectionContext) into the Connection/transport details for each request/response. This means that a GrpcService method may be invoked for multiple connections, from different threads, and even concurrently.

HTTP Filters

As gRPC module is built on top of HTTP module, one can use the HTTP service filters if required to intercept the HTTP layer.

gRPC Filters

gRPC Service Filters have been deprecated and will be removed in a future release. Please use HTTP service filters or implement the interception logic in the particular service definition if decoded protos are required.

In addition to HTTP filters, gRPC users can also add gRPC filters which follow the same interface definition as the service and can be composed using the generated GrpcServiceFactory for a particular service definition.

Client

A Client is created via the GrpcClients static factory. It manages multiple Connections via a LoadBalancer. The control flow of a request/response can be visualized in the below diagram:

                                                                                   +--------------+     +----------------------+     +--------+
                                                                              /--->| Connection 1 |<--->| HTTP Decoder/Encoder |<--->| Socket |
                                                                              |    +--------------+     +----------------------+     +--------+
+--------+ request  +---------+       +--------+ request  +--------------+    |
|  gRPC  |--------->|  gRPC   |------>|  HTTP  |--------->|              |    |    +--------------+     +----------------------+     +--------+
| Client |          | Decoder |       | Client |          | LoadBalancer |<---+--->| Connection 2 |<--->| HTTP Decoder/Encoder |<--->| Socket |
|        |<---------| Encoder |<------|        |<---------|              |    |    +--------------+     +----------------------+     +--------+
+--------+ response +---------+       +--------+ response +--------------+    |
                                                                              |    +--------------+     +----------------------+     +--------+
                                                                              \--->| Connection x |<--->| HTTP Decoder/Encoder |<--->| Socket |
                                                                                   +--------------+     +----------------------+     +--------+

The LoadBalancer is consulted for each request to determine which connection should be used.

HTTP Filters

As gRPC module is built on top of HTTP module, one can use the HTTP client filters if required to intercept the HTTP layer.

gRPC Filters

gRPC Client Filters have been deprecated and will be removed in a future release. Please use HTTP service filters or implement the interception logic in the particular service definition if decoded protos are required.

In addition to HTTP filters, gRPC users can also add gRPC filters which follow the same interface definition as the service and can be composed using the generated GrpcClientFactory for a particular service definition.

Connection Filters

gRPC clients support adding connection filters similar to the HTTP Client.

Service Discovery

gRPC client uses Service Discovery to discover instances of the target service similar to the HTTP Client.

Error Handling

When a Service fails, ServiceTalk maps the Throwable to a GrpcStatus that is sent to the client as the grpc-status and (optionally) grpc-message trailers. Well-known exception types are mapped to specific status codes; any other exception is mapped to UNKNOWN.

Returning an error from a service

To return a meaningful, client-facing error, a Service should fail with a GrpcStatusException that it constructs explicitly — either by throwing it (blocking APIs) or by completing the Single/Publisher with it (asynchronous APIs). The status code and description you put on it are sent verbatim on the wire.

For most errors the ServiceTalk-native GrpcStatus (a status code and an optional human-readable description) is all you need:

// Sent as the grpc-status / grpc-message trailers:
throw new GrpcStatusException(new GrpcStatus(GrpcStatusCode.INVALID_ARGUMENT, "id must be positive"));

When you need to attach structured, typed error detail (the gRPC richer error model), use GrpcStatusException.of(…​) with a com.google.rpc.Status — the details (a repeated Any) are carried in the grpc-status-details-bin trailer. This is what the Application Errors example demonstrates, returning an INVALID_ARGUMENT with a com.google.rpc.BadRequest detail.

The description and details you supply here are not redacted — they are intended for the client and are transmitted as-is. Treat them as untrusted output: include only information that is safe to expose to the remote peer. Anything that is not an explicit GrpcStatusException is redacted instead, as described next.

Exception detail redaction

To avoid leaking internal information to remote peers, the server does not echo arbitrary exception detail in the grpc-message description. For an otherwise-unmapped exception the client receives an opaque reference of the form internal error (ref: <uuid>), and for a serialization failure it receives Serialization error (ref: <uuid>). The full exception (including the same reference) is always logged on the server, so operators can correlate a client-reported reference with the corresponding server log entry without exposing sensitive information.