これはScala Advent Calendar(ADVENTAR)の3日目の記事です。
昨日は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 からも依存されている)
  • 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月時点)」です。

Copyright© 2011-2016 Shunsuke Otani All Right Reserved .