これはScala Advent Calendar(ADVENTAR)の3日目の記事です。
昨日はaoiroaoinoさんの「Imp と implicitly」でした。
以前finagle-http 6.39.0 と jackson-databind 2.6系以外 (に依存するライブラリ)を共存させるという記事を書くにあたってfinagle-toggleというものの存在を知ったので、そもそもこいつは何ぞやというのを動かしながら調べてみた。
昨日はaoiroaoinoさんの「Imp と implicitly」でした。
以前finagle-http 6.39.0 と jackson-databind 2.6系以外 (に依存するライブラリ)を共存させるという記事を書くにあたってfinagle-toggleというものの存在を知ったので、そもそもこいつは何ぞやというのを動かしながら調べてみた。
finagle-toggleとはFinagleにFeature Toggles(Feature Flags)の機能を持たせるものらしい。
(ちなみにこれをAdvent Calendarのネタにしようと思った後の出来事だけど、Developers Festa Sapporo 2016の北海道だけに俺たちの本気を見せてやるぜ!~ マイクロソフトとOSSごった煮 DevOps 衝撃デモシリーズ!2016 ~という発表で"Trunk-based Development", "Feature Flags", "デプロイとリリースを分離する" あたりの話を聞けて、ナイスタイミングだったのでブログ記事タイトルに一部使わせていただいた。)
今回使用するコード全体は以下の通り。(finagle-toggle は finagle-http からも twitter-server からも依存されている)
fractionが0.5なので、だいたい旧機能と開発中機能が使用される割合が半分くらいになっている。
finagle-httpでの設定や、それを上書きしている事なども確認できる。
サーバーの再起動なく、
明日はxuwei-kさんの「コミッターをしているScalaのライブラリ一覧(2016年12月時点)」です。
(ちなみにこれをAdvent Calendarのネタにしようと思った後の出来事だけど、Developers Festa Sapporo 2016の北海道だけに俺たちの本気を見せてやるぜ!~ マイクロソフトとOSSごった煮 DevOps 衝撃デモシリーズ!2016 ~という発表で"Trunk-based Development", "Feature Flags", "デプロイとリリースを分離する" あたりの話を聞けて、ナイスタイミングだったのでブログ記事タイトルに一部使わせていただいた。)
今回使用するコード全体は以下の通り。(finagle-toggle は finagle-http からも twitter-server からも依存されている)
- build.sbt
scalaVersion := "2.11.8" libraryDependencies ++= Seq( "com.twitter" %% "finagle-http" % "6.40.0", "com.twitter" %% "twitter-server" % "1.25.0" )
- src/main/scala/com/zaneli/toggle/Server.scala
package com.zaneli.toggle import com.twitter.finagle.{Http, Service, SimpleFilter} import com.twitter.finagle.http.{MediaType, Method, Request, Response, Status} import com.twitter.finagle.http.service.RoutingService import com.twitter.finagle.stats.DefaultStatsReceiver import com.twitter.finagle.toggle.StandardToggleMap import com.twitter.finagle.util.Rng import com.twitter.server.TwitterServer import com.twitter.util.{Await, Future} object Server extends TwitterServer { private[this] val toggleMap = StandardToggleMap("com.zaneli.toggle", DefaultStatsReceiver) private[this] val experimentalFilter = new SimpleFilter[Request, Response] { override def apply(req: Request, service: Service[Request, Response]): Future[Response] = { if (toggleMap("com.zaneli.toggle.UseExperimental")(Rng.threadLocal.nextInt())) { Experimentals.routes(req) } else { service(req) } } } def main(): Unit = { val server = Http.serve(":8080", experimentalFilter andThen Services.routes) onExit { server.close() } Await.ready(server) } object Services { private[this] val greeting = new Service[Request, Response] { override def apply(req: Request): Future[Response] = { val res = Response(req.version, Status.Ok) res.contentType = MediaType.PlainText res.contentString = "Hello!" Future.value(res) } } private[Server] val echo = new Service[Request, Response] { override def apply(req: Request): Future[Response] = { val res = Response(req.version, Status.Ok) req.contentType.foreach(res.contentType = _) res.contentString = req.contentString if (res.contentString.isEmpty && toggleMap("com.zaneli.toggle.NoContentIfEmpty")(Rng.threadLocal.nextInt())) { res.status = Status.NoContent } Future.value(res) } } private[Server] val routes: Service[Request, Response] = RoutingService.byMethodAndPath { case (Method.Get, "/greeting") => greeting case (Method.Post, "/echo") => echo } } object Experimentals { import java.time.LocalDateTime private[this] val greeting = new Service[Request, Response] { override def apply(req: Request): Future[Response] = { val now = LocalDateTime.now() val content = now.getHour match { case h if 6 to 10 contains h => "Good morning!" case h if 11 to 17 contains h => "Good afternoon!" case h if 18 to 22 contains h => "Good evening!" case _ => "Good night..." } val res = Response(req.version, Status.Ok) res.contentType = MediaType.PlainText res.contentString = content Future.value(res) } } private[Server] val routes: Service[Request, Response] = RoutingService.byMethodAndPath { case (Method.Get, "/greeting") => greeting case (Method.Post, "/echo") => Services.echo } } } - src/main/resources/com/twitter/toggles/configs/com.twitter.finagle.http-service.json
{ "toggles": [ { "id" : "com.twitter.finagle.http.UseNetty4", "fraction" : 1.0 } ] } - src/main/resources/com/twitter/toggles/configs/com.zaneli.toggle.json
{ "toggles": [ { "id": "com.zaneli.toggle.UseExperimental", "description": "for switch to experimental service.", "fraction": 0.5 }, { "id": "com.zaneli.toggle.NoContentIfEmpty", "description": "for switch to 204 status if response body is empty.", "fraction": 0.0 } ] }
/echoと/greetingがあり、/greetingには常に「Hello」を返す旧機能と、現在時間によって返すメッセージを変える開発中機能があるというてい。fractionが0.5なので、だいたい旧機能と開発中機能が使用される割合が半分くらいになっている。
> curl "http://localhost:8080/greeting" Good afternoon! > curl "http://localhost:8080/greeting" Hello! > curl "http://localhost:8080/greeting" Good afternoon! > curl "http://localhost:8080/greeting" Hello!
/echoはボディをそのまま返すだけ。
> curl "http://localhost:8080/echo" -X POST -d 'ping' -w '\n%{http_code}\n'
ping
200
> curl "http://localhost:8080/echo" -X POST -w '\n%{http_code}\n'
200
この設定値はTwitterServerが提供する管理用サーバーを使ってブラウザなどで確認できる。http://localhost:9990/admin/toggles/で全設定を、http://localhost:9990/admin/toggles/com.zaneli.toggle/com.zaneli.toggle.UseExperimentalのようにidをURLに含めるとその設定内容を確認することができる。finagle-httpでの設定や、それを上書きしている事なども確認できる。
> curl "http://localhost:9990/admin/toggles/"
{
"libraries" : [
{
"libraryName" : "com.twitter.finagle.http",
"toggles" : [
{
"current" : {
"id" : "com.twitter.finagle.http.serverErrorsAsFailuresV2",
"fraction" : 0.0,
"description" : "Treat responses with status codes in the 500s as failures. See `com.twitter.finagle.http.service.HttpResponseClassifier.ServerErrorsAsFailures`."
},
"components" : [
{
"source" : "jar:file:/Users/zaneli/.ivy2/cache/com.twitter/finagle-http_2.11/jars/finagle-http_2.11-6.40.0.jar!/com/twitter/toggles/configs/com.twitter.finagle.http.json",
"fraction" : 0.0
}
]
},
{
"current" : {
"id" : "com.twitter.finagle.http.serverErrorsAsFailures",
"fraction" : 0.0,
"description" : "Superseded by `com.twitter.finagle.http.serverErrorsAsFailuresV2`"
},
"components" : [
{
"source" : "jar:file:/Users/zaneli/.ivy2/cache/com.twitter/finagle-http_2.11/jars/finagle-http_2.11-6.40.0.jar!/com/twitter/toggles/configs/com.twitter.finagle.http.json",
"fraction" : 0.0
}
]
},
{
"current" : {
"id" : "com.twitter.finagle.http.UseNetty4",
"fraction" : 1.0,
"description" : "Use netty4 as the transport implementation"
},
"components" : [
{
"source" : "file:/Users/zaneli/ws/private/toggle-example/target/scala-2.11/classes/com/twitter/toggles/configs/com.twitter.finagle.http-service.json",
"fraction" : 1.0
},
{
"source" : "jar:file:/Users/zaneli/.ivy2/cache/com.twitter/finagle-http_2.11/jars/finagle-http_2.11-6.40.0.jar!/com/twitter/toggles/configs/com.twitter.finagle.http.json",
"fraction" : 0.0
}
]
}
]
},
{
"libraryName" : "com.twitter.finagle.netty4",
"toggles" : [
{
"current" : {
"id" : "com.twitter.finagle.netty4.poolReceiveBuffers",
"fraction" : 0.0,
"description" : "Enable pooling of receive buffers on Netty 4"
},
"components" : [
{
"source" : "jar:file:/Users/zaneli/.ivy2/cache/com.twitter/finagle-netty4_2.11/jars/finagle-netty4_2.11-6.40.0.jar!/com/twitter/toggles/configs/com.twitter.finagle.netty4.json",
"fraction" : 0.0
}
]
}
]
},
{
"libraryName" : "com.zaneli.toggle",
"toggles" : [
{
"current" : {
"id" : "com.zaneli.toggle.UseExperimental",
"fraction" : 0.5,
"description" : "for switch to experimental service."
},
"components" : [
{
"source" : "file:/Users/zaneli/ws/private/toggle-example/target/scala-2.11/classes/com/twitter/toggles/configs/com.zaneli.toggle.json",
"fraction" : 0.5
}
]
},
{
"current" : {
"id" : "com.zaneli.toggle.NoContentIfEmpty",
"fraction" : 0.0,
"description" : "for switch to 204 status if response body is empty."
},
"components" : [
{
"source" : "file:/Users/zaneli/ws/private/toggle-example/target/scala-2.11/classes/com/twitter/toggles/configs/com.zaneli.toggle.json",
"fraction" : 0.0
}
]
}
]
}
]
}
また、設定値の変更もこの管理用サーバーで行うことができる。NoContentIfEmptyの設定値を変更して、空文字であれば204を返すように変更してみよう。
> curl "http://localhost:9990/admin/toggles/com.zaneli.toggle/com.zaneli.toggle.NoContentIfEmpty?fraction=1.0" -X PUT
{
"message" : "Update successful",
"errors" : [ ]
}
> curl "http://localhost:9990/admin/toggles/com.zaneli.toggle/com.zaneli.toggle.NoContentIfEmpty"
{
"libraries" : [
{
"libraryName" : "com.zaneli.toggle",
"toggles" : [
{
"current" : {
"id" : "com.zaneli.toggle.NoContentIfEmpty",
"fraction" : 1.0,
"description" : "for switch to 204 status if response body is empty."
},
"components" : [
{
"source" : "Mutable(com.zaneli.toggle)",
"fraction" : 1.0
},
{
"source" : "file:/Users/zaneli/ws/private/toggle-example/target/scala-2.11/classes/com/twitter/toggles/configs/com.zaneli.toggle.json",
"fraction" : 0.0
}
]
}
]
}
]
}
サーバーの再起動なく、
/echoの挙動が変わる事が確認できた!
> curl "http://localhost:8080/echo" -X POST -d 'ping' -w '\n%{http_code}\n'
ping
200
> curl "http://localhost:8080/echo" -X POST -w '\n%{http_code}\n'
204
明日はxuwei-kさんの「コミッターをしているScalaのライブラリ一覧(2016年12月時点)」です。