PlayFrameworkしながらgRPCサーバも起動する

前回の記事で基本的なgRPCサーバの起動はできるようになった.

h3poteto.hatenablog.com

gRPCで提供するメソッド群はgRPCサーバで使えるように .proto ファイルをどんどん増やしていけば良い. でもたまに,RESTも提供したいけどgRPCでメソッド叩けるようにもしたい,みたいなことを思うことがある.

というわけで,今度はPlayFrameworkでRESTの応答を受け付けながら,gRPCサーバも起動してみる.

playとgRPC

その前に,今回対象とするのはPlayFramework 2.6である.

実はPlayFramework2.5ではgRPCサーバを立ち上げることが出来ない.

stackoverflow.com

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

github.com

悩みどころ

開発環境で,$ sbt run しただけだと,まだコンパイルは走らない. localhost:9000 にアクセスすると,そこで初めてコンパイルされ,サーバが起動する.

そのため,gRPCサーバもこのタイミングまでは,起動することがない.

ここをなんとかしたい.

たとえば,gRPCサーバへの初回アクセス時に,ちゃんとコンパイルが走ってサーバが起動するとかなら良いのだが.

PlayFrameworkの,この辺のHotReloadの機構についてはまだちゃんと追えてない.