Overview

Hello gRPC! (with ScalaPB)

No Comments

gRPC is a modern RPC framework developed by Google. It picks up the traditional idea of RPC frameworks – call remote methods as easily as if they were local – while trying to avoid mistakes made by its predecessors and focusing on requirements of microservice-oriented systems. gRPC has been heavily utilized by Google for several years and has seen its first public release (1.0.) in August 2016.

Some of gRPC’s highlights include

  • be highly performant by leveraging new features introduced by HTTP/2 and favoring protocol buffers over JSON
  • avoid the need to write boiler-plate code by providing a simple IDL (Interface Definition Language) and code generation based on protocol buffers
  • support blocking (synchronous) and non-blocking (asynchronous) calls and bi-directional streaming
  • enable flow-control at application level to handle unbalanced producer/consumer speeds
  • support for polyglot services with official libraries for 10+ languages and even more community provided languages

gRPC workflow in a nutshell

Per default, gRPC uses Protocol Buffers as its IDL (Interface Definition Language) and message format. So the – somewhat simplified – steps involved in creating a simple application with a service and client are:

  • Describe your service, client and messages in a .proto file using proto3 syntax
  • Use the protoc compiler with the gRPC extension to generate the service, client and message source code in your preferred programming language
  • On the server side: Implement the service interfaces/traits (no boiler-plate just business logic)
  • On the client side: Use the generated stubs to call the service

Languages supported out-of-the-box

Currently gRPC officially supports the following languages: C++, Java, Python, Go, Ruby, C#, NodeJS, Android Java, Objective-C and PHP

…and what about Scala?

ScalaPB to the rescue!

ScalaPB (Scala Protocol Buffers) is a protocol buffer compiler plugin for Scala, i.e. it creates Scala source code from .proto files. Luckily, there’s also a nice little extension that allows us to generate the Scala sources needed to implement gRPC services and clients.

Enough talking, let’s see some code

The snippets shown in this blog post only contain the relevant/important pieces of the complete application. You can find a repository containing the full source code of the provided examples at https://github.com/pbvie/hello-grpc.

Setup

As mentioned above, we’ll be using ScalaPB to generate Scala sources from our service and message definitions written using protocol buffers.

ScalaPB provides an sbt plugin to integrate the compilation of proto files into the usual Scala development workflow. Since we’re going to use gRPC, we’ll also add the required plugin to generate gRPC related sources from protocol buffers.

To add both plugins we need to edit the following files:

plugins.sbt

addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.3")
libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.5.46"

build.sbt

// import ScalaPB
import com.trueaccord.scalapb.compiler.Version.scalapbVersion
 
// add these ScalaPB settings to your current settings
PB.protoSources.in(Compile) := Seq(sourceDirectory.in(Compile).value / "proto"),
PB.targets.in(Compile) := Seq(scalapb.gen() -> sourceManaged.in(Compile).value),
libraryDependencies ++= Seq(
  "com.trueaccord.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf",
  "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" % scalapbVersion,
  "io.grpc" % "grpc-netty" % "1.0.1"
)

Please see https://github.com/pbvie/hello-grpc/blob/master/project/plugins.sbt and https://github.com/pbvie/hello-grpc/blob/master/build.sbt for the complete setup.

After adding the plugins, running sbt compile will automatically generate and compile Scala sources from .proto files located in src/main/proto/. To only generate the Scala sources (without compiling them) you can run sbt protoc-generate.

Single request/response

The simplest scenario of client/server communication consists of the client sending a single request to the service and receiving a single response in return.

We’ll implement a simple SumService that returns the sum of two numbers passed to it in a request.

Service and message definition

service Sum {
  rpc CalcSum (SumRequest) returns (SumResponse) {}
}
 
message SumRequest {
  sint32 a = 1;
  sint32 b = 2;
}
 
message SumResponse {
  sint32 result = 1;
}

The first section defines our service. In this case we’re defining a simple RPC method with a single request of type SumRequest and a single response of type SumResponse. Below we define the content of the request and response: Our request will contain two integers called a and b and our response will contain a single integer called result.

Now switch to your terminal (inside your project directory) and execute:

sbt compile

ScalaPB should have generated the following files:

  • target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumGrpc.scala
  • target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumProto.scala
  • target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumRequest.scala
  • target/scala-2.12/src_managed/main/io/ontherocks/hellogrpc/sum/SumResponse.scala

The most interesting file here is SumGrpc.scala since it contains the traits and classes needed to implemented our service and client. If you open the generated file you will find, among others, the following definitions:

trait Sum // the trait we'll use to implement our service
trait SumBlockingClient // the trait implemented by the client stubs
class SumBlockingStub // a blocking (synchronous) client stub
class SumStub // a non-blocking (asynchronous) client stub

Service implementation

Now that we have our generated code in place, let’s use it to implement our simple SumService.

class SumService extends SumGrpc.Sum {
  def calcSum(request: SumRequest): Future[SumResponse] = {
    val result = SumResponse(request.a + request.b)
    Future.successful(result)
  }
}

Thanks to gRPC we don’t have to concern ourselves with any boiler-plate code and can concentrate on implementing the – in this case rather simple – business logic.

Client implementation

In the last step we add our client calling the service. gRPC has generated two different kinds of client stubs for us: A blocking/synchronous client as well as a non-blocking/asynchronous one. Below we’ll use both to perform the same service call.

Both clients can use the same channel and request, so we only need to define them once:

val channel = ManagedChannelBuilder.forAddress(Host, Port).usePlaintext(true).build
val request = SumRequest(3, 4)

Blocking/Synchronous client

val blockingSumClient: SumBlockingStub = SumGrpc.blockingStub(channel)
val blockingSumResponse: SumResponse = blockingSumClient.calcSum(request)

Non-blocking/Asynchronous client

val asyncSumClient: SumStub = SumGrpc.stub(channel)
val asyncSumResponse: Future[SumResponse] = asyncSumClient.calcSum(request)

In the first (blocking) example we’re getting back a SumResponse directly, which is only possible by waiting for the response, hence blocking the current thread. In the second (non-blocking) examle we’re getting back a Future[SumReponse] that will eventually contain our response without blocking the current thread.

Server-side streaming

Streaming works similar to single request/response services, so we’ll only cover the parts that are different. For a complete example please see the provided GitHub repository.

We’ll implement a little ClockService that returns a stream of the current time (in ms) every second for the next 10 seconds.

Service and message definition

Again, we start by defining our service and the exchanged messages in a .proto file:

service Clock {
  rpc GetTime(TimeRequest) returns (stream TimeResponse) {}
}
 
message TimeRequest {}
 
message TimeResponse {
  int64 currentTime = 1;
}

The important difference to our previous example is that we define the response of our service as a stream of TimeResponse messages.

Service implementation

After ScalaPB generates the necessary sources for us, we can implement our service:

class ClockService extends ClockGrpc.Clock {
  def getTime(request: TimeRequest, responseObserver: StreamObserver[TimeResponse]): Unit = {
    val scheduler = Executors.newSingleThreadScheduledExecutor()
    val tick = new Runnable {
      val counter = new AtomicInteger(10)
      def run() =
        if (counter.getAndDecrement() >= 0) {
          val currentTime = System.currentTimeMillis()
          responseObserver.onNext(TimeResponse(currentTime))
        } else {
          scheduler.shutdown()
          responseObserver.onCompleted()
        }
    }
    scheduler.scheduleAtFixedRate(tick, 0l, 1000l, TimeUnit.MILLISECONDS)
  }
}

Instead of directly returning a Future, that will eventually contain our response, we now pass our responses to the StreamObserver.onNext method. When we’re done sending messages we call the StreamObserver.onComplete method to notify the client that the stream has completed.

Client Implementation

Blocking/Synchronous client

val blockingClockClient: ClockBlockingStub = ClockGrpc.blockingStub(channel)
val blockingClockResponse: Iterator[TimeResponse] = blockingClockClient.getTime(request)
for (t <- blockingClockResponse) {
  println(s"[blocking client] received: $t")
}
// remaining code will only be executed AFTER the stream has completed (in this case blocking our thread for 10 seconds!)

Non-blocking/Asynchronous client

val asyncClockClient: ClockStub = ClockGrpc.stub(channel)
val timeResponseObserver = new StreamObserver[TimeResponse] {
  def onNext(value: TimeResponse) = println(s"[async client] received: $value")
  def onError(t: Throwable) = println(s"[async client] error: $t")
  def onCompleted() = println("[async client] stream completed")
}
 
asyncClockClient.getTime(request, timeResponseObserver)
 
// remaining code will be executed immediatelly without waiting for the stream to be completed

While both clients will produce roughly the same output when run, the first (blocking) client will block the thread until the whole stream has completed. The async client registers an observer/callback that will handle the streamed messages as they arrive without blocking the current thread.

What’s next

Congratulations! You have written your first services using gRPC using two of the most common styles of communication: single request/response and server-side streaming. Furthermore, gRPC supports the definition of client-side streaming as well as bi-directional streaming. Both work similar to the provided server-side example and should be easy to implement with the help of the official documentation (see next section).

Follow-up material and links

Comment

Your email address will not be published. Required fields are marked *