サブスクリプションの管理SaaSを提供しているアルプ株式会社でエンジニアをしています、@showmant です。
これはScala Advent Calendar 2019 14日目のエントリです。
#はじめに
サービスを運用していると、場合によってはサービスを停止し、メンテナンス時間を入れることがあると思います。
今回はAkka HTTPで運用しているアプリケーションのメンテナンス時のハンドリングについて簡単にご紹介させてください。
ユースケース
メンテナンス時にメンテナンス中
であることをクライアントに返却する
やりたいこと
- confの切り替えによってメンテナンス中か否かを制御したい
- これにより、ある程度柔軟にメンテナンスモードに入ることができる
-
RejectionHandlerを使ってレスポンス制御したい
- Routingをシンプルにし、RejctionHandlerにロジックを集中することで、Routingが間違っていて事故が起こるようなことを避けたい
実装
Custom Rejection の実装
標準でもAuthのRejectionをはじめ、いくつものRejectionを搭載していますが、今回はカスタムのRejectionクラスを作ってみます。作り方はRejectionを継承すればいいだけですので、簡単です。
import akka.http.scaladsl.server.Rejection
case object MaintenanceRejection extends Rejection
final case class MaintenanceRejection() extends Rejection
Rejection Handlerの実装
RejectionHandlerは ドキュメントのサンプルのように書けばいいのですね。 もちろん entity
の部分をJSONレスポンスに変更したりもできます。
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import sample.akkaHttp.MaintenanceRejection
RejectionHandler.newBuilder()
.handle{
case MaintenanceRejection =>
complete(HttpResponse(ServiceUnavailable, entity = "メンテナンス中です"))
}
//他のRejectionの制御方法をここに追加していけばよい
.handleNotFound { complete((NotFound, "Not here!")) }
.result()
Routingの実装
Routingでどのような条件分岐でreject
をするかはプロダクトによって大きく変わってくると思います。例えば、hcは通すけど、それ以外はメンテナンスモードにいれる、というようなこともあると思います。
今回は本題に集中するため、それらは省いてシンプルにメンテナンスモードのON/OFF
で分岐する実装をサンプルとしました。
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import sample.MaintenanceRejection
class SampleRouter(
conf: SampleConf
) {
override def routes: Route =
extract(ctx => ctx.request) { request =>
if (conf.isInMaitenance) reject(MaintenanceRejection)
else ??? //メンテナンスモードでなければこちらでハンドリングする
}
}
サーバーの実装
最後にサーバー立ち上げ時に、RejectionHandler
の読み込みと、Routing
を差し込めばOKです。
object SampleApiServer extends App {
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
implicit def myRejectionHandler =
RejectionHandler
.newBuilder()
.handle {
case MaintenanceRejection =>
complete(HttpResponse(ServiceUnavailable, entity = "メンテナンス中です"))
}
//他のRejectionの制御方法をここに追加していけばよい
.handleNotFound { complete((NotFound, "Not here!")) }
.result()
val route = (new SampleRouter).routes
Http().bindAndHandle(route, "localhost", 9000)
}
おわりに
実際のプロダクトでは、特にRoutingをどのようにわけるか、他のRejectionが一緒に積まれた場合はどうするかなど、考えるべきことがもう少しあるかのように思います。
記事を書き始めると意外とシンプルであまり中身がない感じになってしまいましたが、どなたかの参考になれば幸いです。
あしたは @grimrose@github さんです!