ブログ・ア・ラ・クレーム

技術的なメモとかライフログとか。

mrb_state を跨いで Proc オブジェクトをやり取りしたい話

この記事は mruby Advent Calendar 2016 11 日目の記事です。

Advent Calendar のネタとしてはやっぱりイカした mrbgem を作りました的な内容をブチ上げて行きたいところですが、残念ながらそのようなネタは思いつかず。 本記事では自分が拙作プロダクト ts_mruby を実装中ハマった問題をちょろっと共有するシンプルな内容になっています。

背景

ことのおこり

ts_mruby は Apache Traffic Server(以下ATS) に組み込むプラグインとして、 mod_mruby, ngx_mruby のような役割を担うことを目的に開発を続けています。 また、今年 9 月に バージョン 0.1 をリリースしています。

ATS は主な用途が HTTP プロキシサーバであり、プラグイン向けにいくつかのフックが用意されています。 ts_mruby-0.1 ではそのうちクライアントから ATS へリクエストが発生したタイミングでの mruby スクリプトによる制御はサポートしていたのですが、その他の、例えばオリジンサーバへリクエストを転送するタイミングやレスポンスを受け取るタイミングのフックに関しては、サポートが非常に弱い状態になっています。

f:id:syu_cream:20161211160808p:plain

このフックがサポートできないと、例えばレスポンスヘッダに関する情報の参照や修正など重要ないくつかの操作を受け付けることができません。 そこで ts_mruby-0.2 (今年中にリリース予定)ではこのサポートを厚くすることにしました。 具体的には下記のように mruby スクリプトからフックとイベントハンドラを登録可能にするクラス ATS::EventSystem を実装し、フックの動作タイミングでイベントハンドラを実行するという方針で対応することにしました。

class MyHandler 
  def on_send_request_hdr
    hout = ATS::Headers_out.new
    hout["x-on_send_request_hdr"] = "1"
  end

  def on_read_response_hdr
    hout = ATS::Headers_out.new
    hout["x-on_read_response_hdr"] = "2"
  end

  def on_send_response_hdr
    hout = ATS::Headers_out.new
    hout["x-on_send_response_hdr"] = "3"
  end
end

es = ATS::EventSystem.new
es.register MyHandler

上記の MyHandler クラスの各メソッドは、 ATS のフック における TS_HTTP_SEND_REQUEST_HDR_HOOK, TS_HTTP_READ_RESPONSE_HDR_HOOK, TS_HTTP_SEND_RESPONSE_HDR_HOOK に相当しています。

問題

ATS::EventSystem を実装するのは良いですが、下記の 2 点に配慮すべき事項があります。

まず ATS はマルチスレッド・イベント駆動で処理します。 そのためもちろんながらスレッドを跨いでリソースを共有する際はロックを取る必要がある場面が生じます。 また、先述のフックを処理するスレッドがどれになるかは不定です。

また、 ts_mruby はロックを取ることによる実装の煩雑さとロックを取ることによるパフォーマンス低下をきらい、スレッドローカルに mrb_state インスタンスを持つ設計にしています。

f:id:syu_cream:20161211160829p:plain

上記 2 点に配慮すると、最初のフックで ATS::EventSystem を用いてフックとイベントハンドラを仕込んでも、その後のフックを処理するスレッドがイベントハンドラを知らないといけなくなります。

f:id:syu_cream:20161211161049p:plain

というわけで (別のスレッドがもつ)mrb_state インスタンスに、(イベントハンドラの) Proc オブジェクトを渡したい という要求が発生しました。

irep を dump することによるスレッド間の Proc オブジェクト受け渡し

irep の dump

Proc オブジェクト(C の型としては RProc) は実行する命令などの情報を保持する mrb_irep 型のメンバを持ちます。 上記の問題を解決するため、今回は Proc オブジェクトの irep からバイトコードを dump し、それをスレッド間で持ち回すことを考えます。

RProc の mrb_irep メンバをバイトコードに戻す関数 mrb_dump_irep() が mruby/dump.h に宣言されています。 mrb_dump_irep() dump したバイトコードを、格納するのに必要な領域を mrb_malloc() で確保しつつ、 uint8_t* 型で返却します。

ここで取り出したバイトコードは同じく mruby/dump.h に宣言されている mrb_read_irep() で mrb_irep 型に戻すことができます。 後はこれを mrb_closure_new() で RProc 型に変換してから mrb_obj_value()mbr_value 型の Proc オブジェクトに変換し、最後に mrb_yield() で proc オブジェクトを実行すれば元のイベントハンドラの実行まで完了です。

全体の流れを図示すると、下記のようになります。

f:id:syu_cream:20161211161132p:plain

ts_mruby への機能追加

上記で検討した irep を dump することによるイベントハンドラ受け渡しですが、実際の実装としては下記のようになります。 下記のソースコードは断片的なものですが、実際に組み込んだものを既に ts_mruby の master ブランチ にマージ済みなのでそちらでご確認いただけます。

イベントハンドラ登録処理

イベントハンドラを登録する側の処理は下記のようになります。 下記では省いていますが、 ATS 固有のフックの登録も行う必要があります。 また、 send_request_hdr_irep_ に格納したバイトコードはどうにかしてイベントハンドラを処理するスレッドに渡るようにする必要があります。 ts_mruby としては HTTP トランザクション処理に紐づくデータを登録できるので、そこに格納しています。

static const char* SEND_REQUEST_HDR_HANDLER  = "on_send_request_hdr";
...
mrb_sym sym = mrb_intern_cstr(mrb, SEND_REQUEST_HDR_HANDLER);
RProc* rproc = mrb_method_search(mrb, rclass, sym);

uint8_t* send_request_hdr_irep_ = NULL;
size_t binsize = 0;
mrb_dump_irep(mrb, rproc->body.irep, DUMP_ENDIAN_NAT, &send_request_hdr_irep_, &binsize);

イベントハンドラ実行処理

イベントハンドラを処理する側の処理は下記のようになります。 (どのようにかして) send_request_hdr_irep_ を取り出し、前述の通りの変換を行って最終的に mrb_yield() で実行します。

uint8_t* handler_irep = ... // send_request_hdr_irep_

RProc* closure = mrb_closure_new(mrb, mrb_read_irep(mrb, handler_irep));
mrb_value proc = mrb_obj_value(closure);
mrb_value rv = mrb_yield(mrb, proc, mrb_nil_value());

所感

今回紹介したやり方ですが、やはり Proc オブジェクトを一旦 irep に dump した後に再度 irep に戻すという変換を行っているので、ややコストが高いです。 これとは別のやり方として下記の方法で Proc オブジェクトの mrb_value を直接複数スレッド複数 mrb_state インスタンス間でやり取りすることも検討し、一度は実装していました。

  • イベントハンドラ登録処理を実行するスレッドの持つ mrb_state インスタンスで Proc オブジェクトを生成する

    • この際に GC に回収されないよう mrb_gc_register()GC root に登録しておく
  • イベントハンドラ実行スレッドで先程の mrb_value を直接参照し、処理する

  • 処理が終わったらスレッドセーフな「mbr_value を使い終わりましたメッセージ」を格納するキューにメッセージ追加
  • イベントハンドラ登録処理を実行したスレッドが次回実行する際、メッセージを確認する

    • 使い終わった mrb_value に対しては mrb_gc_unregister()GC root から取り外す

ただしこの方針は実装が汚くなりがちで、あまりコストも低くないであろうことから廃止してしまいました。

おわりに

あまり一般的な問題という訳ではないでしょうが、本記事が誰かの役に立つと嬉しいです :D また、本記事の内容に要改善点などあればご指摘いただけると嬉しいです! (ちゃんと調べてないですが、本記事の工夫の代わりに mruby-marshal を使うなども有用かも?)