ぷりちゃんわ!キラッとThriftやってみた!

前置きしてみた!

――ということで、今回は履修管理システムで採用予定になっているThriftの話です。
もし使い方を調べてやってきた方は公式ドキュメント(GitHub公式サイト)を読むのがいいかなと思います。
ざっくり言えば、Thrift.XXXTransportでtransportを作って、Thrift.YYYProtocolでprotocolを作って、Thriftで自動生成(ex.thrift --gen js hoge.thrift)したメソッドでclientを作るという具合になっています。
必要に応じて引数をObjectにして、Optionを持たせることが出来るような仕組みになっています。

注意事項書いてみた!

今回の記事は最低限JSの知識があるのを前提にしていますので、もし知らないよという方はMDNを全力で読んでください。
なお筆者はブロッキングの処理に疎いです。Thrift内ではネットワークを介すため、本来であれば深く考慮すべき事項です。
また読んでいるとわかりますが、ソースコードと対面しつつ読んでいかないと厳しいところがあります。
Thriftファイルは以下のとおりなので、git clone git@github.com:apache/thrift.gitなどでThriftのコードも落としておいてください。
生成コードがあると便利かもしれません。もちろん、1.0.0-devを触れる方はビルドも必要です。

本編

今回やることまとめてみた!

まず、ThriftのCallbackをPromiseに書き換えたいという思いが叶うのか、自動生成ファイルを読みつつライブラリの実装にも触れていきます。
次に1.0.0-devにおけるPromise実装を試しに触れてみたいと思います。XHRとWSで挙動が異なることも多いですが、そのあたりにも言及します。

Thriftについて理解してみた!

現在の最新版である0.11.0においては、clientから叩くAPIの受けとりは let hoge = client.sendMessage();や、client.sendMessage((res)=>{hoge = res;});というように、返り値を受け取るパターンとコールバックで処理する方法のみがサポートされています。
また、現在のマスターブランチで開発中の1.0.0(現在は1.0.0-dev)において、Promise(client.sendMessage().then(res=>{}))がサポートされる予定となっています。

CallbackをPromiseに書き換えようとしてみた!

大体のプロジェクトにおいて、この状況で1.0.0-devを採用するのは重いと思います。
そんな中、Promiseを解決したいという思いからCallbackを書き換えようとするかもしれません。
ひとまずここで、実家のようなDefault.jsを読むことで、実装可能なのか判断していきましょう。

内部実装なんで知らなくていいよ><という方は、後半までスクロールしてサンプルをご覧ください。

自動生成ファイルを追いかけてみた!

ver 0.11.1

ver 1.0.0-dev

とても簡単な実装ではありますが、send_Ping()new Promiseで囲うか囲わないかという具合です。
――send_Pingはちょっと長めなので転載はしませんが、挙動はほぼ変わらず、Promiseの恩恵としてTry Catchが出来るようになっているという具合です。

通信捌く部分を読んでみた!

じゃあどこで通信周りをさばいているのかという話ですが、それがThriftライブラリ内にあります。
このライブラリは

  1. プロトコル内にあるwriteMessageBeginからtstackという配列にデータを格納
  2. writeMessageEndにおいてそれらをtransport.writeを通じバッファに移動
  3. getTransport().flushによってXMLHttpRequest.send()WebSocket.send()を叩いて通信をする

という処理が行われています。

書き換え可能か検討してみた!

さて、話を戻して0.11.0でCallbackからPromiseに書き換えたいという話ですが、おそらくどちらでも不可能(もしくはハックが必要)だと考えられます。
ライブラリや自動生成ファイルに手を加えないことが大前提として話しますが、主な理由は解決のタイミングです。

XHR版flushのコールバックが解決されるのは、通信完了で発火するonreadystatechangeによります。
そのイベントが発火したタイミングで動く処理として、コールバックが消化される形になります。

WS版flushのコールバックは手順は複雑なものの、本質的にはPromise外でコールバックが消化されるためXHR版と同様の解決タイミングです。

WS版flushの処理手順まとめてみた!

  1. transport.open()時にWebSocketのonmessageを自らの__onMessageに割り当てる
  2. hoge()が叩かれると、内部でsend_hoge()が叩かれる
  3. send_hoge()内で先述した通りバッファ等の処理をして、flushを叩く
  4. 真っ先にsocket.sendして、受けとりバッファの登録とコールバックを実行するfunctionをcallbacks[]に格納する
  5. (このタイミングで既に一旦flushの処理は終わり、次に書かれている処理に進み、ブロックされない)
  6. onmessageが発火したとき、__onMessageが呼ばれ、callbacks内に入れておいた処理を実行する

実際に試したコード(Get版のみ一部抜粋)が以下のとおりです。

このとき、callbacksに含まれるのは*MessageRes=>{resolve(*MessageRes);}であり、これではチェーンすることはありません。
ただ、何故か送信(Send)処理の場合は正しくthenにチェーンしました。
自動生成ファイルも覗きましたが最終的にたどり着くのはflushであり、このような差が生まれた理由がわかりません。
もし今後理解できたら続編を書かせていただきます。そろそろSpiderMonkeyやV8になる時間が来たのかもしれません。技量不足で申し訳ありません。

結論

どちらにおいてもcallbackの解決を待つことはなく、Promiseを作って解決しようとしても、callbackが解決される(=resolve()が発行される)前にPromise外の処理が進みます。
もし後にonreadystatechangeonmessageが発火しても、callbacks内の関数からresolve()を叩いたところでPromiseが解決されることはありません。

send_GetMessageの解決を待機して、解決後にthenにチェーンさせたいため、ver1.0.0-devでの自動生成ファイルのようにnew Promise()するしかないと考えられます。
new Promise()は内部を非同期で処理します。そのため、thenにチェーン可能な状態を維持したまま、その後の処理を止めることなく実行可能です。

――それでもPromise……使いたい。やってみなくちゃわからない。わからなかったらやってみよう!
ということで次に1.0.0-devにおけるサンプルの制作をまとめてみます。

サンプル制作やってみた!

今回作るサンプルでは、マスターブランチを使った複数の受け取り方におけるJSの書き方をまとめます。
驚くほどに簡単です。現在における一番の難所はビルドです。

コールバック版(XHR/WS)

Promise版(XHR/WS)

このような具合でコールバックを受け取ったり、Promiseの解決をすることが出来ます。
本来こんなに単純だったはずなのですが、Promiseを解決したいという欲求のせいでライブラリを追っかけてしまいました。

まとめ

インターフェイスという概念を半自動でこなしてくれるというのがThriftにおける最高の利点です。
また複数の環境でクライアントを実装する場合や、複数人での開発においても便利ではないでしょうか。
今回はコールバックからPromiseに置き換えようという話が主体でしたが、おそらく次のバージョンではPromiseがサポートされることになり、フロントエンドでも更に触れやすくなることが期待されます。
今回触れたのは履修管理システムでの採用を考えてのことでしたが、内部実装を読んだりする中で様々な知見を得られました。
特に旧時代のPrototypeを見れたり、同期・ブロッキングあたりに触れることが出来たのは大きな経験になりました。

現在、アクティブなメンバーが圧倒的に不足しているソフトウェア工房ですが、こういった普段触れにくい技術にも増える機会がたくさんあります。
ぜひプログラミング、こんぴうた、開発が大好きな方はソフトウェア工房までお越しください。

超絶長い記事でしたが、ここまで読んでいただけた方がいましたらありがとうございます。今後もよろしくおねがいします。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)