finch
TRANSCRIPT
Introduction of Finch
What is Finch?Finagle の Service を簡単に関数型指向で
書けるようにするための Combinator ライブラリ
Service
HTTP Server
HTTP ClientFinch
Build using CombinatorsFilter
finagle
≒Request => Future[Response]
Why Finch?様々なプロトコルに汎化されている
Finagle と違って HTTP 特化 Routing が書きやすい HTTP 向けの Utility が豊富 最近の JSON ライブラリ (Circe とか Argonaut) をサポート Service 以外のレイヤー (Filter や Server) は そのまま Finagle のものを利用可能
Differences in Code
GET /div/op1/(int1)/op2/(int2)
{ "result": (int)}=>
op1 を op2 で割った結果を返す
Finagle
import io.circe.generic.auto._import io.circe.syntax._case class Res(result: Int)
val service: Service[Request, Response] = RoutingService.byMethodAndPathObject[Request] { case (Get, Root / "div" / "op1" / op1 / "op2" / op2) => new Service[Request, Response] { def apply(request: Request): Future[Response] = Future.value( allCatch withTry { Response(request.version, Status.Ok, Reader.fromBuf( Buf.Utf8( Res(op1.toInt / op2.toInt).asJson.noSpaces ) )) } getOrElse Response(request.version, Status.BadRequest, Reader.fromBuf( Buf.Utf8("Invalid params") )) ) }}
1. Routing に RoutingService + Pattern matching が必要…
1
2. 軽いエラーハンドリングもメインのロジックの中に 埋め込む必要がある
2
23
3. 明示的に JSON 化する必要がある
Finch
case class Res(result: Int)
val getDiv: Endpoint[Res] = get( "div" :: "op1" :: int :: "op2" :: int.shouldNot("be 0") { _ == 0 }) { (op1: Int, op2: Int) => Ok(Res(op1 / op2))}
import io.finch.circe._import io.circe.generic.auto._val service: Service[Request, Response] = getDiv.toService
1. Routing がサクッとかける !
2. 軽いエラーハンドリングは簡単に書ける ! ( 少なくともメインのロジックとは分離した書き方ができる )3. Finch が内部で勝手に JSON にエンコードしてくれる !
What is Endpoint in Finch?Finch で Service を実装する ≒ Endpoint を実装
する
Endpoint[A]
val getDiv: Endpoint[Res] = get( "div" :: "op1" :: int :: "op2" :: int.shouldNot("be 0") { _ == 0 }) { (op1: Int, op2: Int) => Ok(Res(op1 / op2))}
Request => Option [ Future[ Output[A] ] ]HTTP Response に対応
非同期処理でラップ(Backend での処理に対応 )
A の有無に対応
(50x のエラーはここで発生 )
(404 のエラーはここで発生 )
Relation to Finagle ServiceEndpoint を直列や並列に並べたものが
Finagle の Service になる
Endpoint 1
Endpoint 3
::
Endpoint 4
:+:
:+:Finagle Service
Endpoint 2
toService
How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling
How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling
RoutingMethod
各 HTTP Method に対応した Utility がある.
Path直列に繋げる (1 つのエンドポイントとして構成する ) 時
は :: を使う
並列に繋げる ( 別のエンドポイントとして構成する ) 時は:+: を使う
get("div" :: "op1" :: int :: "op2" :: int)GET /div/op1/(int)/op2/(int)
get("hello" :: string) :+: post("echo" :: string)
GET /hello/(string) POST /echo/(string)
Routing - extracting user inputspremitive な型は組み込みの Utility メソッドがあ
る.Path params
• string, long, int, boolean, uuidQuery params
• param, paramOption, params, paramsNelBody
• body, bodyOption, binaryBody, binaryBodyOption, asyncBody
get("div" :: "op1" :: int :: "op2" :: int :: paramOption("pretty").as[Boolean])
GET /div/op1/(int)/op2/(int)[?pretty={true|false}]
etc…
Routing - extracting user inputsExtract したパラメータは Endpoint#apply メ
ソッドで 引数として受けられる.
get( "div" :: "op1" :: int :: "op2" :: int :: paramOption("pretty").as[Boolean]){ (op1: Int, op2: Int, isPretty: Boolean) => ??? }
Column about Routing (1)既存のエンドポイントを直列に繋げる例
val getSumOfProdAndDiv: Endpoint[Int] = get(getProd :: getDiv) { (product: Int, division: Int) => Ok(product.result + division.result)}
val getDiv: Endpoint[Int] = get("div" :: "op1" :: int :: "op2" :: int) { (op1: Int, op2: Int) => Ok(op1 / op2)}
val getProd: Endpoint[Int] = get("prod" :: "op1" :: int :: "op2" :: int) { (op1: Int, op2: Int) => Ok(op1 * op2)}
GET /prod/op1/(int)/op2/(int)/div/op1/(int)/op2/(int)
※ 別 Method のエンドポイントを直列につなぐと 何にもマッチしないエンドポイントになるので注意
Column about Routing (2):+: による結合順序は区別がある.
val hello: Endpoint[String] = get("hello" :: string) { (str: String) => Ok(s"Hello $str!!!") }
val helloBar: Endpoint[String] = get("hello" :: "bar") { Ok("bar")}
hello :+: helloBar のとき
GET /hello/bar => “Hello bar!!!”(1)
helloBar :+: hello のとき
GET /hello/bar => “bar”(1)
パスのマッチングは結合順の前からチェックされる
How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling
JSON Serialization (Circe)次の 2 つをやっておけば Finch が勝手にやってく
れるtoService と同じスコープに Encoder[A] を implicit で
定義toService と同じスコープに io.finch.circe._ を importcase class Res(result: Int)
object CalcService { val getDiv: Endpoint[Res] = get("div" :: "op1" :: int :: "op2" :: int) { (op1: Int, op2: Int) => Ok(Res(op1 / op2)) }}
object ServerApp extends TwitterServer { import io.finch.circe._ import io.circe.generic.auto._ val endpoints = CalcService.getDiv.toService . . .}
こっちじゃなくて
ココ !!!
※ Circe の場合 io.circe.generic.auto._ を import しておくだけで case class の Encoder を生成してくれるのでいい感じです !!!
Not JSON Response普通に使うと全レスポンスが application/json に
エンコードされてしまう…val hello: Endpoint[String] = get("hello" :: string) { who: String => Ok(s"Hello $who")}
GET /hello/bar => ”Hello bar”単一の String Value を返す場合でも””で囲まれるのでウザい…
val helloPlain: Endpoint[Response] = get("helloPlain" :: string) { who: String => val res = Response() res.setContentType("text/plain") res.setContentString(s"Hello $who") Ok(res)}
GET /helloPlain/bar => Hello barResponse を返す Endpoint にして
Content-Type 等設定すれば一応回避できる
How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling
Validation for User Inputsパスの各 Extractor に付随して書ける.
get("div" :: "op1" :: int :: "op2" :: int.shouldNot("be 0") {_ == 0})
=> 2 つ目のパラメータが 0 の時に NotValid をスロー
メインロジック部分からバリデーションコードを排除できて good!!!
※ ちなみに NotValid をスローした場合, Finch は勝手に BadRequest を返してくれます.
How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling
Error Handling未キャッチ例外は handle combinator で捕捉可能
キャッチされない例外がある場合はメッセージなしの InternalServerError で終わってしまいます…
val getDiv: Endpoint[Res] = get( "div" :: "op1" :: int :: "op2" :: int) { (op1: Int, op2: Int) => Ok(Res(op1 / op2))} handle { case ae: ArithmeticException => BadRequest(ae)}
Error Handling自前の例外を JSON として返したい場合,
Finch デフォルトの Encoder[Exception] をoverride する必要がある.
case class ErrorRes(errorCode: Int, message: String) extends Exception
import io.circe.syntax._ import io.finch.circe._import io.circe.generic.auto._ implicit val errorEncoder: Encoder[Exception] = Encoder.instance { case er: ErrorRes => er.asJson}
val endpoints = CalcService.getDiv.toService
※override しているので,上記だけだと, Finch 組み込みの例外 (NotValid, NotPresent, NotParsed, etc…) が処理されなくなってしまいます. 実運用ではその辺りも case に追加するのがベターです.
Entire code (some imports are omitted)
case class Res(result: Int)case class ErrorRes(errorCode: Int, message: String) extends Exception
object CalcService { val getDiv: Endpoint[Res] = get("div" :: "op1" :: int :: "op2" :: int) { (op1: Int, op2: Int) => Ok(Res(op1 / op2)) } handle { case ae: ArithmeticException => BadRequest(ErrorRes(400, ae.getMessage)) }}
object ServerApp extends TwitterServer {
import io.finch.circe._ import io.circe.generic.auto._ import io.circe.syntax._
implicit val errorEncoder: Encoder[Exception] = Encoder.instance { case er: ErrorRes => er.asJson } val endpoints = CalcService.getDiv.toService
val server: ListeningServer = Http.server.serve(":8000", endpoints) onExit(Await.ready(server.close(30 seconds))) Await.ready(server)}
Performanceベンチとってみました.
div のエンドポイントを OpenStack mille の i2.largeでホスト
Apache bench で `ab –n 100000 –c 1` Finch Finagle
2874.29 [req/sec] 3139.83[req/sec]
Finch は Finagle の 91% ぐらいのスループット
公式ドキュメント通り Finagle そのままの方が若干早い
このオーバーヘッドが大きいと見るかは用途次第か…
Pros. and Cons.Pros.
ルーティングが楽JSON サポートが厚い関数型で書けるのでテストしやすいハズ…
Cons.10% ぐらいのオーバーヘッドがある.JSON Encoder のスコープがとっ散らかると
なかなか原因を見つけにくいIntelliJ が Shapeless の高度な型プログラミングに
ついてこれない悲しさ…