前回の記事で基本的なgRPCサーバの起動はできるようになった.
gRPCで提供するメソッド群はgRPCサーバで使えるように .proto
ファイルをどんどん増やしていけば良い.
でもたまに,RESTも提供したいけどgRPCでメソッド叩けるようにもしたい,みたいなことを思うことがある.
というわけで,今度はPlayFrameworkでRESTの応答を受け付けながら,gRPCサーバも起動してみる.
playとgRPC
その前に,今回対象とするのはPlayFramework 2.6である.
実はPlayFramework2.5ではgRPCサーバを立ち上げることが出来ない.
playが裏側で使うNettyとgRPCが裏側で使うNettyがコンフリクトする.
PlayFramework2.6では,裏側がakka-httpになったため(gRPCは相変わらずNettyを使う),コンフリクトしなくなっている.
build.sbt
前回と同じ project/plugins.sbt
を用意しておく.
build.sbtに関しては,前回とあまり変わらないものを書く.PlayFrameworkのbuild.sbtに,必要な分だけgRPCの設定を追加する.
import com.trueaccord.scalapb.compiler.Version.{grpcJavaVersion, scalapbVersion, protobufVersion} // 中略 lazy val root = (project in file(".")).enablePlugins(PlayScala) libraryDependencies ++= Seq( guice, jdbc, evolutions, "com.typesafe.play" %% "play" % "2.6.3", "com.typesafe.play" %% "play-jdbc-api" % "2.6.3", "mysql" % "mysql-connector-java" % "5.1.43", "io.grpc" % "grpc-netty" % grpcJavaVersion, "com.trueaccord.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf", "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" % scalapbVersion, "io.grpc" % "grpc-all" % grpcJavaVersion, ) PB.targets in Compile := Seq(scalapb.gen() -> ((sourceManaged in Compile).value / "protobuf-scala")) PB.protoSources in Compile += (baseDirectory in LocalRootProject).value / "protocol"
サーバファイルの置き場所
前回の記事では,src/main/scala/grpc/server.scala
にgRPCサーバを記述した.
しかし,PlayScalaのpluginを使っていると,この場所はコンパイルしてくれない.
どこでもいいのだが,app/grpc/server.scala
みたいな場所を自分でつくろう.
trait Runner { def start(): Unit } class RunnerImpl @Inject() (actorSystem: ActorSystem, lifecycle: ApplicationLifecycle)(implicit exec: ExecutionContext) extends Runner { val server = new GrpcServer(exec) def start(): Unit = { server.start() server.blockUnitShutdown() } // playが終了するときに呼ばれる lifecycle.addStopHook { () => Future.successful(server.stop()) } actorSystem.scheduler.scheduleOnce(1.seconds) { start() } } // サーバ定義は省略
で,こんなのを追加する. traitを用意したのはplay起動時にInjectするためである.
やってみるとわかるが,この中で単に start()
するだけだと,このクラスのコンストラクタはいつまで経っても終了することがない.
そのため,playのサーバは起動せずいつまで経ってもhttpのリクエストを受け付けることがない.
そのため,
actorSystem.scheduler.scheduleOnce(1.seconds) {
start()
}
して,actorでgRPCのサーバを起動している.
それに伴い,
lifecycle.addStopHook { () => Future.successful(server.stop()) }
している.
これはすごく重要で,たとえばふだんplayの開発をしていると,$ sbt
で起動した後 run
とかしていると思う.
$ sbt [info] Loading project definition from /home/akira/src/github.com/h3poteto/play-grpc-example/project [info] Set current project to play-grcp-example (in build file:/home/akira/src/github.com/h3poteto/play-grpc-example/) [play-grcp-example] $ run
で,playを止めたいときはそこで, Ctrl+Dする.
このとき,playのライフサイクルを捕まえてgRPCサーバを停止させないと,actorで動いているgRPCサーバはそのままJVMの終了まで生き残り続ける.
これではコードを書き換えるたびにJVMを再起動しなければならないので,開発しにくい.
play起動時にgRPCサーバを起動する
次に,app/Module.scala
でInjectさせる.
class Module extends AbstractModule { override def configure(): Unit = { bind(classOf[Runner]).to(classOf[RunnerImpl]).asEagerSingleton } }
こうして,Injectして,singletonにしておくことで,play起動時に RunnerImpl
のコンストラクタが呼ばれてgRPCサーバがスタートする.
だいたいこれでplayの起動と同時にgRPCサーバが起動するようになった.
[play-grcp-example] $ run --- (Running the application, auto-reloading is enabled) --- [info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Enter to stop and go back to the console...) [info] application - [][] Creating Pool for datasource 'default' [info] p.a.d.DefaultDBApi - [][] Database [default] connected at jdbc:mysql://127.0.0.1:3306/play_grpc_example?characterEncoding=UTF8&connectionCollation=utf8mb4_bin&useSSL=false [info] p.a.h.EnabledFilters - [][] Enabled Filters (see <https://www.playframework.com/documentation/latest/Filters>): [info] play.api.Play - [][] Application started (Dev) [info] grpc - [][] gRPC server started, listening on 50051
悩みどころ
開発環境で,$ sbt run
しただけだと,まだコンパイルは走らない.
localhost:9000
にアクセスすると,そこで初めてコンパイルされ,サーバが起動する.
そのため,gRPCサーバもこのタイミングまでは,起動することがない.
ここをなんとかしたい.
たとえば,gRPCサーバへの初回アクセス時に,ちゃんとコンパイルが走ってサーバが起動するとかなら良いのだが.
PlayFrameworkの,この辺のHotReloadの機構についてはまだちゃんと追えてない.