Day 20: Dexador
これは fukamachi products advent calendar 2016 の20日目の記事です。
今日はDexadorについて話します。
Drakma
Common Lispには古くからDrakmaというHTTPクライアントがあります。Quickdocs によると74のライブラリで利用されており広く使われているようです。
Edi Weitzと言えばCommon Lispをやっている人のほとんどは知っているでしょう。cl-ppcreやHunchentootのようなテクニカルであり高速であり品質の高いライブラリを多く作っています。
そしてDrakmaもEdi Weitzのプロダクトです。
じゃあDrakmaも彼のプロダクトならば信頼できるか、と言うと、実は期待するほどは品質が高くありません。Drakmaを使ってハマったという人も少なくないのではないでしょうか。
たとえばこんな感じ。
よしdrakma:http-request
にURLを渡せばいいのかな。簡単だわ。あれ、日本語クエリ含む場合にちゃんとGETしてくれない。なんか自動でASCIIでデコード・エンコードされるなあ。PURIの問題? ふむふむ。事前にUTF8 でエンコードした上で:preserve-uri t
を渡せばいいらしい。よしできた。…あれ、エンコーディングエラーが出てる。Shift_JISのページは対応してないのかー。じゃあ:force-binary t
を渡して自分でBabelでデコードするしかないなあ。Content-Typeヘッダを雑にパースして…。よしできた。…あれ、意図しないHTMLが返ってきてる。うーん、手元ではうまくいくんだけどなあ。ん? たまに503が返ってきてるみたいだ。じゃあステータスコードを見て4xxと5xxのときはエラーにしなきゃなあ。できれば何度かリトライもしたい。
とかやってるとコードがとても複雑になってきます。我々はただHTTPリクエスト投げたいだけなのに。
この辺りは後のLisp Meetupでの発表資料に詳しくまとめています。
曖昧にDrakmaで使いづらいと思っている内容を明確にリストアップしています。
twitter.comClear explanations of Drakma's Pitfalls https://t.co/CoXxojYH6V
— PuercoPop (@PuercoPop) 2015年8月26日
こういった状況を改善すべく、よりよいAPIを提供することでDrakmaを置き換えられないかなと考えました。そこで作ったのが「Dexador」です。
良いAPIでは不十分
新しくHTTPクライアントを作ることは可能でしょうが、使ってもらわなければ意味がありません。その観点から言うと「Drakmaより良いAPIを提供します」というだけでは不十分だとは感じていました。「良い、なんてものは主観的だ」と言われればそれまでです。細かい改善も伝わりづらく、なかなか移行するほどアピールできるとは思えません。
そこで思い至ったのが高速化です。
Wooで作ったfast-httpやQURIを使うことでDrakmaよりも圧倒的に高速になれば優位性が数値として証明できます。
これは良い戦略かと思われたのですが、fast-httpとQURIでは1.2倍程度の速度しか出ませんでした。そこで最大のボトルネックであるコネクション確立部分をスキップすることでさらなる高速化を行いました。
マクロレスエラーハンドリング
Dexadorの売りの一つは4xxや5xx系のステータスコードが返ってきたときはエラーを発生するということです。もしそのエラーハンドリングをしたければhandler-case
やhandler-bind
を使います。
(handler-case (dex:get "http://lisp.org") (dex:http-request-bad-request () ;; 400が返ったときの処理 ) (dex:http-request-failed (e) ;; それ以外のエラーの処理 (format *error-output* "The server returned ~D" (dex:response-status e))))
エラーの無視や自動リトライなどもhandler-bind
でできます。
;; 404は無視する (handler-bind ((dex:http-request-not-found #'dex:ignore-and-continue)) (dex:get "http://lisp.org")) ;; 失敗したら自動リトライ (handler-bind ((dex:http-request-failed #'dex:retry-request)) (dex:get "http://lisp.org")) ;; 失敗したら5回まで自動リトライ (間は3秒間あける) (handler-bind ((dex:http-request-failed (dex:retry-request 5 :interval 3))) (dex:get "http://lisp.org"))
こういった構文は安易な発想をすればマクロを使うシーンです。しかし、独自のマクロは使い方を覚えてもらう必要があり学習コストがかかります。一方でhandler-bind
やhandler-case
は言語仕様の機能なので誰でも使えます。
ここにも11日目でも取り上げたマクロレス思想の一端が見られます。
Drakmaの微妙な不具合
Dexadorを作っている過程でいくつか不具合も出ました。新しいプロダクトだから仕方ありませんね。直しましょう。ところでDrakmaはどうやっているのだろう。試してみると同じ不具合があった、というようなことも多くありました。結果的にDrakmaのあまり知られていない微妙な不具合をいくつか知っています。
twitter.comDrakmaはgzipで返ってきたレスポンスをwant-stream tでストリームで受け取るとかなり面倒なことになるな。
— fukamachi (@nitro_idiot) 2015年7月28日
twitter.comDexadorでkeep-aliveのコネクションをストリームで受け取るとブロッキングする問題が見つかって、これCHUNGAがおかしそうだけどDrakmaはどうやってんだ、って思って試したらDrakmaも同じ挙動だった。Drakmaはデフォルトでcloseだから気づきづらいだけ
— fukamachi (@nitro_idiot) 2015年8月27日
「Drakmaでもコネクション再利用できるよ」なんて言ってくる人なんて今までいませんでしたけど、いたとしたらドキュメントを読んだだけで使ったことがない人なのでしょうね。
おわりに
明日のアドベントカレンダーは21日目、たぶんLackについてです。お楽しみに。