Hello gRPC!

Petra Bierleutgeb

An introduction to gRPC with ScalaPB

@pbvie

short background

gRPC - History

  • successor to Google's internal framework "stubby"
  • developed by Google and Square
  • Version 1.0 (first stable) released on August 23, 2016
  • now at version 1.7
  • project joined CNCF (Cloud Native Computing Foundation) on March 1, 2017

In a nutshell

  • Remote Procedure Call
  • describe services and exchanges messages using an IDL
    (Interface Description Language)
  • gRPC uses proto3 syntax as IDL
  • protocol buffers: serialization (binary) format
  • generate source code (in multiple languages)
    based on IDL
  • server side: implement generated interfaces
  • client side: use stubs to call services

Benefits: efficiency

  • protobuf binary format - compared to JSON:
    • smaller messages
    • fast transportation and fast deserialization
    • deserialization requires less CPU power (than parsing)
  • HTTP/2 based transport
    • connection multiplexing, bi-directional streaming
    • header compression

Recommendation: Talks by Mete Atamel (Google)
https://drive.google.com/file/d/0B6j6Te0viCnHQVlFNUpDOGhlRzg

Benefits: usability

  • declarative service description via IDL
  • code generation
    • no need to write boiler-plate code
    • no more misspelled field names or wrong types (type-safety FTW!)
    • easier to work with new/unknown APIs:
      use your IDEs auto-completion to explore methods
  • good support for versioning and evolving services

getting started

gRPC support in Scala

via ScalaPB

compiler plugin

proto runtime dependencies

gRPC runtime dependencies

ScalaJS support

Spark support

JSON conversion

+ grpc-java

Updating your build

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

libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.6.6"

plugins.sbt

// minimum for protobuf compilation
PB.targets in Compile := Seq(
  scalapb.gen() -> (sourceManaged in Compile).value
)

libraryDependencies ++= Seq(
    // required for customizations and access to google/protobuf/*.proto
    "com.trueaccord.scalapb" %% "scalapb-runtime" 
      % com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf",
    // next two lines are for gRPC
    "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" 
      % com.trueaccord.scalapb.compiler.Version.scalapbVersion,
    "io.grpc" % "grpc-netty" 
      % com.trueaccord.scalapb.compiler.Version.grpcJavaVersion
)

build.sbt

This will...

  • enable code generation and compilation when running sbt compile
    • => look for proto definitions under
           src/main/protobuf
    • => generate Scala source files under
           target/scala-2.12/src_managed
    • => compile these generated sources
  • to just generate sources run sbt protoc-generate

>> let's try it out <<

hello world!

Service definition

syntax = "proto3";

package io.ontherocks.hellogrpc;

service HelloWorld {
  rpc SayHello(ToBeGreeted) returns (Greeting) {}
}

message ToBeGreeted {
  string person = 1;
}

message Greeting {
  string message = 1;
}

src/main/protobuf/helloworld.proto

sbt compile

Generated Files

  • target/scala-2.12/src_managed
    • io/ontherocks/hellogrpc/helloworld
         
      • Greeting.scala
      • HelloWorldGrpc.scala
      • HelloWorldProto.scala
      • ToBeGreeted.scala

response message

request message

service trait, stubs

meta (descriptors)

package

file name

Message classes

  • case class + companion object
  • fields based on proto definition
  • builder methods (withX methods)
  • lenses
  • internal functionality
    • read from CodedInputStream
    • write to CodedOutputStream

Service object

  • service trait
    => will be implemented by concrete services)
  • async stub
    => used by client to call service (async)
  • blocking stub
    => used by client to call service (sync)
  • bindService method
    => bind a concrete implementation to a service

customizations

remember that dependency?

libraryDependencies += "com.trueaccord.scalapb" %% "scalapb-runtime" % 
    com.trueaccord.scalapb.compiler.Version.scalapbVersion % "protobuf"

=> needed for customizations

Package/File options

import "scalapb/scalapb.proto";

option (scalapb.options) = {
  // use a custom Scala package name
  package_name: "io.ontherocks.awesomegrpc"

  // don't append file name to package
  flat_package: true

  // generate one Scala file for all messages (services still get their own file)
  single_file: true

  // add imports to generated file
  // useful when extending traits or using custom types
  import: "io.ontherocks.hellogrpc.RockingMessage"

  // code to put at the top of generated file
  // works only with `single_file: true`
  preamble: "sealed trait SomeSealedTrait"
};

src/main/protobuf/customizations.proto

Primitive wrappers

syntax = "proto3";

package io.ontherocks.hellogrpc;

message ToBeGreeted {
  message Person {
    string name = 1;
  }

  Person person = 1;
  string msg = 2;
}

src/main/protobuf/helloworld.proto

final case class ToBeGreeted(
    person: scala.Option[ToBeGreeted.Person] = None,
    msg: String = ""
    ) extends com.trueaccord.scalapb.GeneratedMessage with ...

src_managed/main/scala/io/ontherocks/hellogrpc/helloworld/ToBeGreeted.scala

Default values

  • in proto3 all fields are optional
  • two kinds of fields:
    Scalar Value Types and Message Types
  • message types are translated `Option`s in Scala
    => missing fields are `None`
  • scalar value types are set to their default value, e.g.:
    • string: "", numbers: 0, boolean: false

"Note that for scalar message fields, once a message is parsed there's no way of telling whether a field was explicitly set to the default value or just not set at all."

Default values

What if we want an Option[String]?

message ToBeGreeted {
  message Person {
    string name = 1;
  }

  Person person = 1;
  string msg = 2;
}

src/main/protobuf/helloworld.proto

final case class ToBeGreeted(
    person: scala.Option[ToBeGreeted.Person] = None,
    msg: String = ""
    ) extends com.trueaccord.scalapb.GeneratedMessage with ...

src_managed/main/scala/io/ontherocks/hellogrpc/helloworld/ToBeGreeted.scala

Primitive wrappers

import "google/protobuf/wrappers.proto";

message ToBeGreeted {
  message Person {
    string name = 1;
  }

  Person person = 1;
  google.protobuf.StringValue msg = 2;
}

src/main/protobuf/helloworld.proto

final case class ToBeGreeted(
    person: scala.Option[ToBeGreeted.Person] = None,
    msg: scala.Option[String] = None
    ) extends com.trueaccord.scalapb.GeneratedMessage with ...

src_managed/main/scala/io/ontherocks/hellogrpc/helloworld/ToBeGreeted.scala

Custom base traits

package io.ontherocks.hellogrpc

trait RockingMessage {
  val rocking = "I rock!"
}

src/main/scala/io/ontherocks/hellogrpc/RockingMessage.scala

syntax = "proto3";

import "scalapb/scalapb.proto";

package io.ontherocks.hellogrpc;

message ToBeGreeted {
  option (scalapb.message).extends = "io.ontherocks.hellogrpc.RockingMessage";

  string person = 1;
}

src/main/protobuf/helloworld.proto

Custom base traits

package io.ontherocks.hellogrpc.helloworld

@SerialVersionUID(0L)
final case class ToBeGreeted(
    person: scala.Option[String] = None
    ) extends com.trueaccord.scalapb.GeneratedMessage 
        with com.trueaccord.scalapb.Message[ToBeGreeted] 
        with com.trueaccord.lenses.Updatable[ToBeGreeted] 
        with io.ontherocks.hellogrpc.RockingMessage { ...

src_managed/main/scala/io/ontherocks/hellogrpc/helloworld/ToBeGreeted.scala

Custom types

message Person {
  string name = 1;
  string birthday = 2 [(scalapb.field).type = "io.ontherocks.hellogrpc.Birthday"];
}

How does gRPC know how to create a Birthday from a String?

Custom types

  • create TypeMapper in companion object of custom type
  • provides functions for conversion to/from custom type
  • must be defined as an implicit
  • alternative: create TypeMapper somewhere else and bring into scope via import

There's more!

communication styles

Communication styles

  • single request/response
  • server-side streaming
  • client-side streaming
  • bi-directional streaming

single request/response

Single request/response

service HelloWorld {
  rpc SayHello(ToBeGreeted) returns (Greeting) {}
}

helloworld.proto

...
  
trait HelloWorld extends _root_.com.trueaccord.scalapb.grpc.AbstractService {
  def sayHello(request: ToBeGreeted): Future[Greeting]
}

...

HelloWorldGrpc.scala

generates

Single request/response

class HelloWorldService extends HelloWorldGrpc.HelloWorld {
  def sayHello(request: ToBeGreeted): Future[Greeting] = {
    val greetedPerson = request.person.getOrElse("anonymous")
    Future.successful(Greeting(message = s"Hello $greetedPerson"))
  }
}

HelloWorldServer.scala

val channel = ManagedChannelBuilder
                .forAddress("localhost", 50051)
                .usePlaintext(true)
                .build
val stub  = HelloWorldGrpc.stub(channel)

stub.sayHello(ToBeGreeted(Some(Person(Some("Bob"))).foreach(println)

HelloWorldClient.scala

server-side streaming

Server-side streaming

service Clock {
  rpc StreamTime(TimeRequest) returns (stream TimeResponse) {}
}

clock.proto

trait Clock extends _root_.com.trueaccord.scalapb.grpc.AbstractService {
  def streamTime(
    request: TimeRequest, 
    responseObserver: _root_.io.grpc.stub.StreamObserver[TimeResponse]
  ): Unit
}

ClockGrpc.scala

generates

NEW

Server-side streaming

class ClockService extends Clock {
  def streamTime(r: TimeRequest, resObserver: StreamObserver[TimeResponse]): Unit = 
    scheduler.scheduleWithFixedDelay(0.seconds, 3.seconds) {
      resObserver.onNext(TimeResponse(System.currentTimeMillis()))
    }
}

ClockServer.scala

val client  = ClockGrpc.stub(channel)

val observer = new StreamObserver[TimeResponse] {
  def onError(t: Throwable): Unit = println(s"Received error: $t")
  def onCompleted(): Unit         = println("Stream completed.")
  def onNext(response: TimeResponse): Unit =
    println(s"Received current time: ${response.currentTime}")
}

client.streamTime(TimeRequest(), observer)

ClockClient.scala

bi-directional streaming

Bi-directional streaming

service Sum {
  rpc Add(stream SumRequest) returns (stream SumResponse) {}
}

sum.proto

  trait Sum extends _root_.com.trueaccord.scalapb.grpc.AbstractService {
    def add(responseObserver: StreamObserver[SumResponse]): StreamObserver[SumRequest]
  }

SumGrpc.scala

generates

NEW

Bi-directional streaming

class SumService extends SumGrpc.Sum {
  def add(responseObserver: StreamObserver[SumResponse]): StreamObserver[SumRequest] =
    new StreamObserver[SumRequest] {
      def onError(t: Throwable): Unit     = ???
      def onCompleted(): Unit             = ???
      def onNext(value: SumRequest): Unit = ???
    }
}

SumServer.scala

val client  = SumGrpc.stub(channel)

val responseObserver = new StreamObserver[SumResponse] {
  def onError(t: Throwable): Unit      = ???
  def onCompleted(): Unit              = ???
  def onNext(value: SumResponse): Unit = ???
}

val requestObserver = client.add(responseObserver)

// call requestObserver.onNext to send/stream requests to server

SumClient.scala

best practices

(opinionated)

Sharing proto definitions

  • create a separate sbt project for proto definitions
  • publish compiled files to a repo of your choice
  • consumers implementing either service or client add that dependency

Sharing compiled proto definitions

  • no need to compile protos or add ScalaPB
    dependencies manually in each client project
  • clean versioning

Pros

  • some ppl don't like to publish generated code
  • limited to Scala
    => not really: source can still be made available in addition to compiled artifcats

Cons (really?)

Generated entities

  • fields of generated messages are always optional
    • => that's probably not what you want :(
    • (almost) always use primitive wrappers
  • services might provide general purpose functionality to multiple clients with different requirements
    • service: keep proto definitions general
    • client: prefer to work with models/types that closely fit your application requirements
    • => don't use generated classes in your domain logic

continuing the journey

Where to go from here

  • we covered most Scala-specific practices/tools
  • there is no Scala implementation of gRPC (yet?)
  • so have a look at grpc-java
    • add headers and/or authentication
    • add interceptors
    • access to more low-level/advanced control
    • ...and more

Useful links

Credits

  • both gRPC and ScalaPB are open source
  • gRPC
    • developed by Google and Square
  • ScalaPB (main contributors)
    • Nadav Samet (trueaccord)
    • Kenji Yoshida

The End

feedback welcome!

@pbvie

Hello gRPC!

By Petra Bierleutgeb

Hello gRPC!

Introduction to using gRPC in Scala with ScalaPB.

  • 187
Loading comments...