finch

26
Introduction of Finch

Upload: seiya-mizuno

Post on 11-Jan-2017

222 views

Category:

Engineering


2 download

TRANSCRIPT

Page 1: Finch

Introduction of Finch

Page 2: Finch

What is Finch?Finagle の Service を簡単に関数型指向で

書けるようにするための Combinator ライブラリ

Service

HTTP Server

HTTP ClientFinch

Build using CombinatorsFilter

finagle

≒Request => Future[Response]

Page 3: Finch

Why Finch?様々なプロトコルに汎化されている

Finagle と違って HTTP 特化 Routing が書きやすい HTTP 向けの Utility が豊富 最近の JSON ライブラリ (Circe とか Argonaut) をサポート Service 以外のレイヤー (Filter や Server) は そのまま Finagle のものを利用可能

Page 4: Finch

Differences in Code

GET /div/op1/(int1)/op2/(int2)

{ "result": (int)}=>

op1 を op2 で割った結果を返す

Page 5: Finch

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 化する必要がある

Page 6: Finch

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 にエンコードしてくれる !

Page 7: Finch

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 のエラーはここで発生 )

Page 8: Finch

Relation to Finagle ServiceEndpoint を直列や並列に並べたものが

Finagle の Service になる

Endpoint 1

Endpoint 3

::

Endpoint 4

:+:

:+:Finagle Service

Endpoint 2

toService

Page 9: Finch

How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling

Page 10: Finch

How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling

Page 11: Finch

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)

Page 12: Finch

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…

Page 13: Finch

Routing - extracting user inputsExtract したパラメータは Endpoint#apply メ

ソッドで 引数として受けられる.

get( "div" :: "op1" :: int :: "op2" :: int :: paramOption("pretty").as[Boolean]){ (op1: Int, op2: Int, isPretty: Boolean) => ??? }

Page 14: Finch

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 のエンドポイントを直列につなぐと 何にもマッチしないエンドポイントになるので注意

Page 15: Finch

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)

パスのマッチングは結合順の前からチェックされる

Page 16: Finch

How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling

Page 17: Finch

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 を生成してくれるのでいい感じです !!!

Page 18: Finch

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 等設定すれば一応回避できる

Page 19: Finch

How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling

Page 20: Finch

Validation for User Inputsパスの各 Extractor に付随して書ける.

get("div" :: "op1" :: int :: "op2" :: int.shouldNot("be 0") {_ == 0})

=> 2 つ目のパラメータが 0 の時に NotValid をスロー

メインロジック部分からバリデーションコードを排除できて good!!!

※ ちなみに NotValid をスローした場合,  Finch は勝手に BadRequest を返してくれます.

Page 21: Finch

How to compose Endpoints? Routing JSON serialization (Circe の場合 ) Validation for data user input Error Handling

Page 22: Finch

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)}

Page 23: Finch

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 に追加するのがベターです.

Page 24: Finch

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)}

Page 25: Finch

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 そのままの方が若干早い

このオーバーヘッドが大きいと見るかは用途次第か…

Page 26: Finch

Pros. and Cons.Pros.

ルーティングが楽JSON サポートが厚い関数型で書けるのでテストしやすいハズ…

Cons.10% ぐらいのオーバーヘッドがある.JSON Encoder のスコープがとっ散らかると

なかなか原因を見つけにくいIntelliJ が Shapeless の高度な型プログラミングに

ついてこれない悲しさ…