Running a gRPC service

Let's assume we have the following gRPC service definition:

syntax = "proto3";

package grpc.hello;

option java_package = "com.example.grpc.hello";

service HelloService {
  rpc Hello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

The Protobuf compiler will produce some Java code under the com.example.grpc.hello package. The most noteworthy one is HelloServiceGrpc.java which defines the base service class we will implement:

public class HelloServiceGrpc {
    ...
    public static abstract class HelloServiceImplBase implements BindableService {
        public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
            asyncUnimplementedUnaryCall(METHOD_HELLO, responseObserver);
        }
        ...
    }
    ...
}

Our implementation would look like the following:

public class MyHelloService extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
        HelloReply reply = HelloReply.newBuilder()
                                     .setMessage("Hello, " + req.getName() + '!')
                                     .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}

GrpcService

Once you've finished the implementation of the service, you need to build a GrpcService using a GrpcServiceBuilder and add it to the ServerBuilder:

import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;

ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
                      .addService(new MyHelloService())
                      .build());
...
Server server = sb.build();
server.start();

Decorating a GrpcService

Unlike a usual Armeria service, GrpcService implements a special interface called HttpServiceWithRoutes. Therefore, it is recommended to decorate a GrpcService by specifying decorator functions as extra parameters rather than using Service.decorate():

import com.linecorp.armeria.server.logging.LoggingService;

ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
                      .addService(new MyHelloService())
                      .addService(new MySecondHelloService())
                      .build(),
           LoggingService.newDecorator());
...
Server server = sb.build();
server.start();

The above decorator decorates all services. If you want to decorate only a specific service, you need to specify the decorators when you add the service to the GrpcServiceBuilder, or use the @Decorator.

GrpcService.builder()
           .addService(new MyHelloService(),
                       List.of(new FirstDecorator(), new SecondDecorator())) // 👈👈👈
           .build()

// Or, use the @Decorator directly to the service or method.
@Decorator(ThirdDecorator.class) // 👈👈👈
class MyHelloService extends MyHelloServiceBase {

    @Decorator(FourthDecorator.class) // 👈👈👈
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        ...
    }
}

As you might have noticed already, the decorators will be applied in the order of FirstDecorator, SecondDecorator, ThirdDecorator, and FourthDecorator.

gRPC-Web

GrpcService supports the gRPC-Web protocol, a small modification to the gRPC wire format that can be used from a browser. Use the gRPC-Web-Client to access the service from a browser.

If the origin of the Javascript and API server are different, gRPC-Web-Client first sends preflight requests by the HTTP OPTIONS method, in order to determine whether the actual request is safe to send in terms of CORS. Armeria provides CorsService to handle this requests, so you need to decorate it when you build a GrpcService:

import com.linecorp.armeria.common.grpc.protocol.GrpcHeaderNames;
import com.linecorp.armeria.server.cors.CorsService;
import com.linecorp.armeria.server.cors.CorsServiceBuilder;

ServerBuilder sb = Server.builder();
...
final CorsServiceBuilder corsBuilder =
        CorsService.builder("http://foo.com")
                   .allowRequestMethods(HttpMethod.POST) // Allow POST method.
                   // Allow Content-type and X-GRPC-WEB headers.
                   .allowRequestHeaders(HttpHeaderNames.CONTENT_TYPE,
                                        HttpHeaderNames.of("X-GRPC-WEB"))
                   // Expose trailers of the HTTP response to the client.
                   .exposeHeaders(GrpcHeaderNames.GRPC_STATUS,
                                  GrpcHeaderNames.GRPC_MESSAGE,
                                  GrpcHeaderNames.ARMERIA_GRPC_THROWABLEPROTO_BIN);

sb.service(GrpcService.builder()
                      .addService(new MyHelloService())
                      .build(),
           corsBuilder.newDecorator(),
           LoggingService.newDecorator());
...
Server server = sb.build();
server.start();

Please refer to Configuring CORS for more information.

Unframed requests

GrpcService supports serving unary RPC methods (no streaming request or response) without gRPC wire format framing. This can be useful for gradually migrating an existing HTTP POST based API to gRPC. As GrpcService supports both binary protobuf and Protobuf-JSON, either legacy protobuf or JSON APIs can be used.

ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
                      .addService(new MyHelloService())
                      .enableUnframedRequests(true)
                      .build());
...
Server server = sb.build();
server.start();

This service's unary methods can be accessed from any HTTP client at e.g., URL /grpc.hello.HelloService/Hello with Content-Type application/protobuf for binary protobuf POST body or application/json; charset=utf-8 for JSON POST body.

Blocking service implementation

Unlike upstream gRPC-Java, Armeria does not run service logic in a separate thread pool by default. If your service implementation requires blocking, run the individual blocking logic in a thread pool, annotate the gRPC services or methods with a @Blocking annotation, wrap the entire service implementation in ServiceRequestContext.current().blockingTaskExecutor().submit(), or set GrpcServiceBuilder.useBlockingTaskExecutor() so the above happens automatically for all service methods and lifecycle callbacks.

Using @Blocking annotation

// You can apply the @Blocking annotation at the class level
// so all methods are executed by ServiceRequestContext.current().blockingTaskExecutor().
@Blocking // 👈
class MyBlockingGrpcServiceImpl extends MyGrpcServiceImplBase {

    public void blockingCall1(...) {
        // Perform a long-running task.
        ...
    }

    public void blockingCall2(...) {
        // Perform a long-running task.
        ...
    }
}

class MyGrpcServiceImpl extends MyGrpcServiceImplBase {

    //  The method will be executed by ServiceRequestContext.current().blockingTaskExecutor().
    @Blocking // 👈
    public void blockingCall(...) {
        // Perform a long-running task.
        ...
    }

    public void nonBlockingCall(...) {
        ...
    }
}

Wrapping a service with ServiceRequestContext.blockingTaskExecutor()

import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.server.ServiceRequestContext;

public class MyHelloService extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void hello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
        ServiceRequestContext.current().blockingTaskExecutor().submit(() -> {
            // Perform a long-running task.

            HelloReply reply = HelloReply.newBuilder()
                                         .setMessage("Hello, " + req.getName() + '!')
                                         .build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        });
    }
}

Using GrpcServiceBuilder.useBlockingTaskExecutor()

ServerBuilder sb = Server.builder();
sb.service(GrpcService.builder()
                      .addService(new MyHelloService())
                      // All service methods will be run within
                      // the blocking executor.
                      .useBlockingTaskExecutor(true) // 👈
                      .build());

Exception propagation

It can be very useful to enable Flags.verboseResponses() in your server by specifying the -Dcom.linecorp.armeria.verboseResponses=true system property, which will automatically return information about an exception thrown in the server to gRPC clients. Armeria clients will automatically convert it back into an exception for structured logging, etc. This response will include information about the actual source code in the server - make sure it is safe to send such potentially sensitive information to all your clients before enabling this flag!

See more details at Client gRPC.

Server Reflection

Armeria supports gRPC server reflection - just add an instance of ProtoReflectionService to your server.

import io.grpc.protobuf.services.ProtoReflectionService;

ServerBuilder sb = Server.builder();
...
sb.service(GrpcService.builder()
                      .addService(new MyHelloService())
                      .addService(ProtoReflectionService.newInstance())
                      .build());
...
Server server = sb.build();
server.start();

For more information, see the official gRPC Server Reflection tutorial.

See also