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
- => look for proto definitions under
- 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
- io/ontherocks/hellogrpc/helloworld
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
- gRPC: http://www.grpc.io
- ScalaPB: https://scalapb.github.io
- Java implementation and examples:
https://github.com/grpc/grpc-java
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.
- 2,968