gRPC in Scala

As many might think, gRPC doesn’t stand for “google Remote Procedure Call” but is a recursive acronym meaning “gRPC Remote Procedure Call”. I don’t know if you buy it but the truth is that is was originally developed by Google and then open-sourced.

If you’ve been in the IT for a while RPC doesn’t necessarily bring back happy memories. On the JVM it all started with RMI in the 90s. RMI was inspired by CORBA and suffered from a lack of interoperability as both the client and the server had to be implemented in Java. RMI was also particularly slow as Java serialisation is not a very efficient protocol.

Later in the 2000s came XML based RPC with XML-RPC and especially SOAP. Both of these formats address the interoperability as it no longer matters how the client/server are implemented. They only need to speak XML. However XML is still not an efficient protocol and communications remain slow.

SOAP provides an interesting definition language (WSDL – Web Service Definition Language) that can be used to generate service implementations.

gRPC addresses all these drawbacks. By default, it uses protobuf (Protocol buffers) for the service definitions and as its serialisation mechanism, which allows it to interoperate with many different languages while providing an efficient serialisation protocol. gRPC also takes advantage of HTTP/2 to add streaming capabilities.

gRPC generates all the boilerplate code for you, so that you only have to implement the business logic inside your services. It supports a number of different languages out of the box: C++, Java (Netty), Python, Go, Ruby, C#, Javascript (Node.JS), Android, Objective-C and PHP.

Unfortunately Scala is not in the list! … but we have scalaPB (and sbt-protoc) to save the day!

Let’s dive in and setup our environment in order to implement the RouteGuide example from grpc-java all in Scala.

Setup

First thing is to add the sbt-protoc plugin to your build. You can edit your plugin.sbt file or create a new protocol.sbt file in the project directory.

In this file you must add the following lines:

addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.9")

libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.6.0-pre5"

Don’t forget to check the latest version!

The next thing to do is to update the build.sbt file:

// compiles protobuf definitions into scala code
PB.targets in Compile := Seq(
  scalapb.gen() -> (sourceManaged in Compile).value
)

libraryDependencies ++= Seq(
  "com.trueaccord.scalapb" %% "scalapb-runtime"      % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf",
  // for gRPC
  "io.grpc"                %  "grpc-netty"           % "1.4.0",
  "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" % com.trueaccord.scalapb.compiler.Version.scalapbVersion,
  // for JSON conversion
  "com.trueaccord.scalapb" %% "scalapb-json4s"       % "0.3.0"
)

And that’s it, we’re all setup now!

Service definition

We are now ready to define our RouteGuide service. ScalaPB automatically looks for protobuf files in src/main/protobuf. By placing our service definitions into this directory we make sure that they’re going to be picked up by sbt-protoc at compile time. (Note that it’s possible to use other folders / jars for the proto files but it requires additional configuration in build.sbt as described in the documentation).

Our service is taken from grpc-java and defines the following calls:

service RouteGuide {
  // A simple RPC.
  // Obtains the feature at a given position.
  // A feature with an empty name is returned if there's no feature at the given
  // position.
  rpc GetFeature(Point) returns (Feature) {}

  // A server-to-client streaming RPC.
  // Obtains the Features available within the given Rectangle.  Results are
  // streamed rather than returned at once (e.g. in a response message with a
  // repeated field), as the rectangle may cover a large area and contain a
  // huge number of features.
  rpc ListFeatures(Rectangle) returns (stream Feature) {}

  // A client-to-server streaming RPC.
  // Accepts a stream of Points on a route being traversed, returning a
  // RouteSummary when traversal is completed.
  rpc RecordRoute(stream Point) returns (RouteSummary) {}

  // A Bidirectional streaming RPC.
  // Accepts a stream of RouteNotes sent while a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}

These calls (while a bit contrived) illustrates the 4 different call types:

  • Unary: a single client/server call. The client sends a request and received a response from the server
  • Server streaming: The client sends a requests and receives a stream of responses from the server
  • Client streaming: The client sends a stream of requests to the client and receives a single response from the server when it finishes processing the stream.
  • Bidirectional streaming: Both the client and servers sends streams of messages to each other

Note how it leverages the HTTP/2 capabilities for streaming messages. All the messages are defined using regular protobuf syntax.

Code generation

gRPC code generation is done with the protocGenerate task but we have added it to the compile phase, so we don’t have to worry about it and can simply run compile to generate the gRPC source files.

Generated sources are created in target/scala_/src_managed which integrates smoothly with IntelliJ.

There is one Scala source file for each of the messages defined by the RouteGuide service. The service itself is generated inside RouteGuideGrpc.

This file contains the following classes:

trait RouteGuide extends AbstractService {
  def getFeature(request: Point): Future[Feature]
  def listFeatures(request: Rectangle, responseObserver: StreamObserver[Feature]): Unit
  def recordRoute(responseObserver: StreamObserver[RouteSummary]): StreamObserver[Point]
  def routeChat(responseObserver: StreamObserver[RouteNote]): StreamObserver[RouteNote]
}

trait RouteGuideBlockingClient {
  def getFeature(request: Point): Feature
  def listFeatures(request: Rectangle): Iterator[Feature]
}

class RouteGuideBlockingStub(channel: Channel, options: CallOptions = CallOptions.DEFAULT) 
extends AbstractStub[RouteGuideBlockingStub](channel, options) 
with RouteGuideBlockingClient { ... }

class RouteGuideStub(channel: Channel, options: CallOptions = CallOptions.DEFAULT) 
extends AbstractStub[RouteGuideStub](channel, options) 
with RouteGuide { ... }
  • RouteGuide> is the trait we have to implement to define the business logic.
  • RouteGuideBlockClient is the trait implemented by the blocking client stub. Note that blocking clients are not able to stream messages from the client to the server.
  • RouteGuideBlockingStub is a blocking (synchronous) stub.
  • RouteGuideStub is a non-blocking (asynchronous) stub.

Writing the business logic

In order to write the business logic all we have to do is to create a class that extends the RouteGuide trait and implements the service methods.

class RouteGuideService(features: Seq[Feature]) extends RouteGuideGrpc.RouteGuide {
  ...

  override def getFeature(request: Point): Future[Feature] = 
    Future.successful(findFeature(request))

  override def listFeatures(request: Rectangle, responseObserver: StreamObserver[Feature]): Unit =
    ...

  override def recordRoute(responseObserver: StreamObserver[RouteSummary]): StreamObserver[Point] =
    ...

  override def routeChat(responseObserver: StreamObserver[RouteNote]): StreamObserver[RouteNote] =
    ...

  ...
}

All the streaming functionality relies on StreamObserver. The control flow is inverted:

  • When the server streams messages to the client, then the observer used to stream server messages is created by the client and passed as an argument of the method
  • When the the client streams messages to the client, the server returns an observer that is then used by the client to trigger onNext events on the server.

Server implementation

With the business logic in place we can now create a server. gRPC server implementation relies on Netty. It’s probably the fastest web server available on the JVM, however it would have been nice to be able to configure the backend to use (e.g. akka-http, …).

However the server creation is pretty straight-forward with the use of the provided builder:

val server: Server = {
  val service = new RouteGuideService(
    RouteGuidePersistence.parseFeatures(featureFile)
  )
  ServerBuilder
    .forPort(port)
    .addService(RouteGuideGrpc.bindService(service, ec))
    .build()
}

We create a new instance of the RouteGuide service and then use the ServerBuilder to add the service.

Client implementation

A client implementation starts with the creation of a Channel. Once we have a Channel we can get a stub and call service methods directly on the stub.

val channel =
  ManagedChannelBuilder
    .forAddress(host, port)
    .usePlaintext(true)
    .build()

val blockingStub = RouteGuideGrpc.blockingStub(channel)
val asyncStub = RouteGuideGrpc.stub(channel)

val feature = blockingStub.getFeature(request)

val responseObserver = new StreamObserver[RouteSummary]() {
  override def onNext(summary: RouteSummary): Unit = { ... }
  override def onError(t: Throwable): Unit = { ... }
  override def onCompleted(): Unit = { ... }
}
val requestObserver = asyncStub.recordRoute(responseObserver)

Conclusion

gRPC addresses many of the shortcomings of previous generations RPCs by providing an efficient serialisation mechanism and bindings in many languages. Although Scala is not in the party, ScalaPB allows us to enjoy the same benefits as the supported languages. ScalaPB also provides the ability to use custom code generators to improve the protobuf support even further and support automatic Json conversion out of the box.

If you have many .proto files it may take a while to compile (code source generation in itself is fast but the compilation of the generated files takes some time). This is especially the case when all the protobuf definitions are gathered in a single project. In this case it might be worth to generate the sources from the protobuf files and publish the compiled classes only. This speed up the compile time of dependent projects plus gives a more robust versioning (because the version of the published jar needs to be incremented for every change in the protobuf files).

Finally, all the source code used in this post is available on github. Enjoy!