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

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

AWS Lambda と CloudWatch を使ってローカルで動かしていたcronの処理をサーバレスにする

普通に生きていると「あっこのバッチ処理定期的に動かしたいな」と思うことがあると思われます。 自分もご多分に漏れずそうだったのですが、私物 PC で cron で回すのも煩わしい(外出中はスリープさせたいので)し、 VPS などでインスタンス借りるのも目的に対してやり過ぎ感があります。 というわけで今回は AWS Lambda + CloudWatch を使って置き換えてみました

前提として

最初は Google Cloud Functions でやろうと思っていたのですが、ただ cron の代替としてちょっとした処理を動かしたい為に結構な労力を払わされそうに見受けられました。

https://developers-jp.googleblog.com/2017/04/how-to-schedule-cron-jobs-with-cloud.html

AWS であれば自分にとって必要最小限のサービスで構成できそうなので乗り換えた次第です。 要件によっては GCP で構成しても良さそうかな・・・と思います。

手順

以下の順序で cron の処理の置き換えをしました。

  • バッチ処理を Node.js で再実装

    • 元々 10 数分で書いたシェルスクリプトだったのですが Lambda でサポートしている言語で再実装しておきました
    • Lambda で実行可能なコードであれば他の言語でも別に良い
  • CloudWatch にイベント登録

    • 以下画面からルールの追加画面に遷移し・・・

f:id:syu_cream:20171009231543p:plain

  • スケジュールをお好みの形で設定

f:id:syu_cream:20171009231655p:plain

  • 後は Lambda で関数が定期的に呼び出されていることを確認

    • 自分が書いた CloudWatch の設定では 1h に一回呼び出されることを想定したものであり、以下のような呼び出し回数グラフが得られました

f:id:syu_cream:20171009231901p:plain

料金

以上で実現したバッチ処理の置き換えですが、料金はどのくらいになるのでしょうか。 自分が書いた Lambda の関数呼び出しは大体 7s 前後で終わっているようで、これを 1h に一回呼び出す状態で運用しています。 この結果、 AWS 請求画面でレポートされる請求に関する情報は以下のとおりになりました。

f:id:syu_cream:20171009232144p:plain

無料枠に収まっているというか全然使いこなせていない感があります。

Mac で ngx_mruby 込みで nginx をビルドする際にいろいろハマってた

ちょっと前に解決した話なんですが備忘録的に書いておきます。 (2017/10/03 記述に漏れがあったので修正しました)

前提

最近の openssl の Formula は link を --force を付けたとしても禁止しています。理解はできるけどサクッとビルドしたい時にやや面倒・・・

$ brew link openssl --force
Warning: Refusing to link: openssl
Linking keg-only openssl means you may end up linking against the insecure,
deprecated system OpenSSL while using the headers from Homebrew's openssl.
Instead, pass the full include/library paths to your compiler e.g.:
  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib

Homebrew の Formula が入らないよ問題

nginx-full Formula はナイスなことにオプションで ngx_mruby を加えてビルドすることができるようになっています。 http://hb.matsumoto-r.jp/entry/2016/02/10/224532

しかし最近以下の点でハマるようになっていました。

  • Homebrew がデフォルトで sandbox-exec してビルドするようになり、パーミッションの都合 --with-mruby-module ではビルドが通らなくなる
    • 一応 --no-sandbox すれば sandbox-exec しなくなるので回避できる
  • ngx_mruby のリンク途中でコケる

前者の問題は本質的には mruby-nginx-module の Formula の構成の問題です。 Homebrew で sandbox-exec してビルドする際、ビルド対象の Formula のディレクトリや /tmp などに書き込み権限を絞ってしまうため、現状の "nginx-full Formula から mruby-nginx-module Formula のビルドを行なう" ような操作が通らなくなります。 これは根が深い問題なので、今回はひとまず後者の問題を回避できるようにしました。

https://github.com/Homebrew/homebrew-nginx/pull/324

これで以下のようにすれば引き続き Homebrew で ngx_mruby をらくらくインストールすることができます。

# 残念ながら --no-sandbox は今のところ必要。しかたないね
$ brew install nginx-full --with-mruby-module --no-sandbox

ngx_mruby をソースからビルドする時困るよ問題

前述の通り openssl のインクルードパスなどを、 ngx_mruby の場合は nginx や mruby のビルド時にも渡るようにしてやらなければならないので一工夫がいります。 幸いにも ngx_mruby の configure, Makefile ではいくつか nginx, mruby のビルド時に任意のオプションを渡すための記述がされているのでそれらを駆使するのが良さそうです。

(2017/10/03 追記・修正) nginx の ビルド時には難ありです。

openssl のソースコードを手元に用意して、 ngx_mruby の --with-ngx-src-root にパスを渡すのがひとつのビルド方法に思われます。

./configure --with-ngx-src-root=/path/to/ngx_mruby/build/nginx_src --with-ngx_config_opt="--with-debug --with-http_stub_status_module --with-http_ssl_module --with-openssl=/path/to/openssl"
...

自分は面倒に感じてしまったので、 nginx の configure に以下のようなパッチを当てて、 Homebrew でインストールした openssl を参照するようにしてしまいました。。。

113a114,129
>         if [ $ngx_found = no ]; then
>
>             # Homebrew
>
>             ngx_feature="OpenSSL library in /usr/local/opt/"
>             ngx_feature_path="/usr/local/opt/openssl/include"
>
>             if [ $NGX_RPATH = YES ]; then
>                 ngx_feature_libs="-R/usr/local/opt/openssl/lib -L/usr/local/opt/openssl/lib -lssl -lcrypto $NGX_LIBDL"
>             else
>                 ngx_feature_libs="-L/usr/local/opt/openssl/lib -lssl -lcrypto $NGX_LIBDL"
>             fi
>
>             . auto/feature
>         fi
>

これを利用すれば以下の通りにビルドできます

$ ./configure --with-ngx-src-root=/path/to/ngx_mruby/build/nginx_src --with-ngx_config_opt="--with-debug --with-http_stub_status_module --with-http_ssl_module
...

$ NGX_MRUBY_CFLAGS="-I/usr/local/opt/openssl/include" NGX_MRUBY_LDFLAGS="-L/usr/local/opt/openssl/lib -lcrypto" make
...
checking for OpenSSL library ... not found
checking for OpenSSL library in /usr/local/ ... not found
checking for OpenSSL library in /usr/pkg/ ... not found
checking for OpenSSL library in /opt/local/ ... not found
checking for OpenSSL library in /usr/local/opt/ ... found
...

かなり遠回り感が出てしまいますが、自分の環境ではこれで無事ビルドできました

ところで

High Sierra では OpenSSL が LibreSSL に置き換わるらしいですね。こんな問題にはもうハマらなくて済むのか、あるいは別のハマりどころに結局苦しめられるのか・・・

mruby を使ってワンバイナリで Serverspec のテストを実行可能にするツール "sssspec" を作った

ながすぎる;よんでない

リポジトリは以下にあります。

以下のように spec を書いて…

def __main__(argv)
  describe file('mrblib') do
    it { should be_readable.by('yourgroup').by_user('you') }
  end
end

sssspec をビルドして…

$ rake

生成されたバイナリひとつで spec を実行することができます

$ ./sssspec
.
Total: 1
   OK: 1
   KO: 0
Crash: 0
 Time: 0.16 seconds

背景

Serverspec はプロセスの起動やパッケージのインストール状態、パッケージの存在確認などサーバの状態をテストするのに有用なツールです。

しかしながらこれをローカル実行するには当然 Ruby 処理系や関連 gem をインストールしておく必要があります。Serverspec の為だけに Ruby を導入する必要が出て来る場合、これはちょっとした障壁となると思われます。 また Serverspec は ssh 経由でリモート実行もできますが、構成上、自動化したい際はデプロイツールの都合、社内ルールの問題で利用できない可能性があります(と、ぼくは思ってるんですが、実際のところそういった制約はどの程度あるのだろう)

Serverspec の導入障壁を減らしたいと考えた僕は、 mruby-cli を利用してワンバイナリで実行可能にした Itamae であるところの MItamae を参考になんとかすることにしました。 そうだ、 mruby 、やろう。

構成など

必要なものたち

mruby で Serverspec を実行するために最低限必要なものは、以下のものと思われます。

  • Serverspec 本体の mrbgem
  • Specinfra の mrbgem
  • RSpec の mrbgem
  • mruby で spec を実行可能にする mruby-cli ベースのコード

これらのうち、 “Specinfra の mrbgem” については @k0kubun さんによって作られた mruby-specinfra が利用できそうです。 というわけで残り 3 コンポーネントを自前で用意することにしました。

mruby-rspec

mruby-rspec は mruby で RSpec を実行可能にする mrbgem です。

経緯の話をすると、当初は Ruby 向けの rspec-core などを mruby 向けに作り直そうと試みたのですが、 RSpec の既存資産がなかなか大きく、 mruby 向けに改修していく作業の途中で心が折れてしまいました。 現在は、限られた機能のみ mruby 向けに移植している(途中だったらしい) sagmor/mruby-spec をベースに mrbgem を実装しています。

mruby-rspec は未だかなり機能が少ないため、本記事の “sssspec” をまともに使い倒して行こうとすると、もう少し機能追加が必要なものと思われます。

mruby-serverspec

mruby-serverspec は本家 Serverspec の matcher, type をほぼ使いまわしで構成した mrbgems です。 ssh 経由でのリモート実行など一部のロジックは削っております。

この辺りは本家 Serverspec のコードを引っ張り出して無理やり mrbgems 化して、動かない箇所を片っ端から修正していくという大変泥臭い作業をして構成しました。

sssspec

sssspec は mruby-cli ベースのテンプレートを修正し、かつこれまで紹介した mrbgems を組み合わせて実現した、ワンバイナリで spec 実行可能なツールです。 実装は 9 割くらい mruby-cli のテンプレそのままで、あり解説すべき点は特に無いので省きます。 ちなみに名前は rrrspec を意識し、 “Single Shot ServerSpec” の略としています。

今後の展望

ひとまず動くところまで作りたかったので、 sssspec と周辺 mrbgems はかなり大雑把な作りになっています。まずは全体的にもう少しリファクタリング等していきたいです。

また現状 mruby-rspec の機能が少ないせいで実用に耐えない気がしています。個人的には少なくとも、 shared_examples が無いと辛いなと思っております。従ってそのあたりの RSpec 便利機能を mruby-rspec にも実装していきたいと思っております。

本記事と関係のない余談

ルビィの話書いてて思い出したんですが、 秋からラブライブ!サンシャイン!! の二期が放映開始されますね! ちなみに僕は渡辺曜ちゃんが好きです。

技術書典2でmrubyに関する薄い本をだします

表題のとおりです。

4/9(日) アキバ・スクエアで開催される 技術書典2 にて mruby に関する薄い本を出します! う-10 サークル名「なるはやでいい感じにやるマン*1です。 https://techbookfest.org/event/tbf02/circle/5686683802533888

主なターゲット読者を Rubyist として、 mruby の言語処理系周り (mruby-compiler とか RiteVM とか) や CRuby との差異 (khash とか GC とか) をいい感じに書いていければと思ってます。 もしご参加の際はぜひよろしくお願いいたします!

*1:なお原稿の進捗具合は、なるはやともいい感じとも言えない模様

KVS をゆるくつなぐ mrbgem "mruby-neco" を作ってみた

あけましておめでとうございます。正月は実家に帰省してネコと戯れて過ごしました。

新年一発目の記事は、新しく作成した mrbgem "mruby-neco" を紹介してみます。

mruby-neco について

mruby-neco は NoSQL Elastic Command Organizer の略としています。はい、言いたかっただけな名前です。

さて mruby-neco ですが、あまた在る NoSQL 、というか特に KVS について、 mruby スクリプト向けにひとまとめしたインタフェースを提供するものです。 具体的には現状下記プロダクトをサポートしています

これらのプロダクトに対して、下記のインタフェースを提供します。

  • get
    • key に対する value を取得する
  • put ,
    • key に対する value を保存する
  • delete
    • key, value のペアを削除する
  • each
    • key, value のペアを受け取るブロックを回すイテレータ
    • Enumerable を include してるので map とか filter とか reduce も使える

それぞれの操作に対して、特定プロダクトを対象にするか、いくつかのプロダクトをひとまとめにした集合を対象にするかを選択できるようにしています

モチベーション

mruby-neco を作ったモチベーションですが、以前の記事で mruby-k2hash というmrbgem を実装した際に、他の KVS から値を持ってきていろいろできてもいいなぁと思ったのと、各プロダクトに付与されている場合がある cli ツールの操作感に差異があり、それを埋める手段が欲しいなぁと思った次第です。 既存の手段でその望みはかなえられる気がしないでもないですが、ここは mruby マンとして mruby でいい感じに KVS 間のデータのやり取りや加工をできると面白いのでは・・・と考えサクッと実装してみました。 ちまたに mrbgem も充実していて、苦労せず実装に踏み出せると考えたという背景もあります。

実際の mruby-neco 利用例

mruby-neco を利用して値を参照更新する例を掲載してみます Org という構造にメンバーを追加し、 Org を介して操作する、みたいな構造を取っています。 Org と各プロダクトのドライバ間の際は Adapter クラスで吸収する形になります。

org = Neco::KvsOrg.new

org.add_member(:etcd, {:url => 'http://127.0.0.1:2379/v2'})
org.add_member(:lmdb, {:path => '/tmp/tmp.lmdb', :options => {:flags => MDB::NOSUBDIR}})
org.add_member(:redis, {:host => 'localhost', :port => 6379})
org.add_member(:k2hash, {:path => '/tmp/tmp.k2hash', :options => {:mode => 0666, :flags => K2Hash::NEWDB}})

org.put_to :etcd, 'key', 'etcd'
org.put_to :lmdb, 'key', 'lmdb'
org.put_to :redis, 'key', 'redis'
org.put_to :k2hash, 'key', 'k2hash'
p org.get_from_all 'key' #=> {:redis=>"redis"}

org.put_to_all 'key', 'value'
p org.get_from_all 'key' #=> {:etcd=>"value", :lmdb=>"value", :redis=>"value", :k2hash=>"value"}

puts 'each_for :redis'
org.each_for(:redis) do |key, value|
  puts "key: #{key}, value: #{value}"
end

puts 'each'
org.each do |key, value|
  puts "key: #{key}, value: #{value}"
end

コマンドラインから neco を利用する

ダンな mruby マンとしては mruby-cli も使ってみたいよね・・・というわけで、 neco-cli という、上記の get と put 操作をコマンドラインから行えるツールを作ってみました。 下記のような使用感になります。

$ neco-cli help
  usage: neco-cli <subcmd>
         neco-cli get <key> [--open <db options as JSON>]
         neco-cli put <key> <value> [--open <db options as JSON>]

  example: neco-cli set "key1" "value1" --open '{"name": redis, "addr": "127.0.0.1", "port": 6379}'

$ neco-cli put key hoge --open '{"name": "redis", "host": "127.0.0.1", "port": 6379}'
$ neco-cli get key --open '{"name": "redis", "host": "127.0.0.1", "port": 6379}'
{"redis":"hoge"}

$ neco-cli put key fuga --open '{"name": "redis", "host": "127.0.0.1", "port": 6379}' --open '{"name": "etcd", "url": "http://127.0.0.1:2379/v2"}'
$ neco-cli get key --open '{"name": "redis", "host": "127.0.0.1", "port": 6379}' --open '{"name": "etcd", "url": "http://127.0.0.1:2379/v2"}'
{"redis":"fuga","etcd":"fuga"}

open オプションの内容と get で得た値のフォーマットは現状 JSON としています。

ネコについて

冒頭で触れた実家のネコですが、 4 ヶ月ほど顔を合わせてなかったにも関わらず僕のことを覚えているようで、全力で甘えに来てくれました(かわいい) しかしながらネコの中で何らかのポリシーがあるのか、あちらから触れてくることは多々あるのですが、こちらから頭を撫でるなど触れるとコレジャナイ感を覚えているような顔をして去っていってしまいます(かなしい)

Yahoo! JAPAN がオープンソースにした KVS "K2HASH" を mruby スクリプトから利用するための mgem "mruby-k2hash" をつくってみた

この記事は mruby Advent Calendar 2016 26 日目の記事です。・・・というのは嘘です! mruby Advent Calendar 2016 、つつがなく完走しましたね!みなさまお疲れ様でした!

本記事では表題の通り、 mruby-k2hash という mgem を作ってみたことに関する共有になります。

K2HASH とは

K2HASHYahoo! JAPANのブログ でちょろっと紹介されている KVS です。 ソースコードGitHub で閲覧することができます。

上記の記事では簡潔な紹介のみになっていますが、 GitHub の wiki を読む限り下記の点から興味深いプロダクトなような気がします。

こうして列挙してみると、個人的な知識から考えるに Redis に近い機能を保有している気がします。 ただデーモンを立ち上げたりファイルに書き出すことすら必須ではないことから、結構気軽に利用できるように見えます。

mruby-k2hash について

基本方針

今回ご紹介する mruby-k2hash はこの K2HASH への操作を mruby スクリプトから行うことを可能にします。 この mruby-k2hash 、基本的には K2HASH の C インタフェースへの橋渡しをするだけになります。ですがその設計は少し考えた上で行っています。 K2HASH の基本的な機能を目を細めてみると・・・うーん・・・なんだか Ruby の標準ライブラリに存在する DBM に近い気がしてきます。

というわけで mruby-k2hash で実装しているメソッドは DBM が持つものを模倣する 形で行っています。 each も実装していることから Enumerable のメソッドも使えるようになっています。 mruby で dbm の mgem を実装したという例は僕が知る限り無い(あったらコッソリ教えてください!)ので、 mruby-k2hash は mruby で気軽に扱える dbm 代替のひとつ としても利用できるのではないかと考えています。

実際の動作例

mruby-k2hash の実際の動作の例としては下記のようになります。

K2Hash クラスのコンストラクタにファイルパスなどなどを渡すとそのファイルを open しつつインスタンスが生成されます。 fetch, [] メソッドで参照、 store, []= メソッドで更新が行えます。

> k2hash = K2Hash.new('/tmp/tmp.k2hash', 0666, K2Hash::WRCREAT)
 => #<K2Hash:0x127ada0>
> k2hash.store('key', 'value')
 => nil
> k2hash.fetch('key')
 => "value"
> k2hash.store('key', 'new value')
 => nil
> k2hash.fetch('key')
 => "new value"
> k2hash['key2'] = 'value2'
 => "value2"
> k2hash['key2']
 => "value2"
> k2hash.close
 => nil

each メソッドや、それを実装したことによって Enumerable インタフェースのメソッド、例えば map なども利用できます。

> k2hash = K2Hash.new('/tmp/tmp2.k2hash', 0666, K2Hash::WRCREAT)
 => #<K2Hash:0xcef7c0>
> k2hash['key1'] = 'value1'
 => "value1"
> k2hash['key2'] = 'value2'
 => "value2"
> k2hash.each do |k,v|; puts k + ': ' + v; end
key1: value1
key2: value2
 => nil
> k2hash.map do |k,v|; "key is #{k}, value is #{v}"; end
 => ["key is key1, value is value1", "key is key2, value is value2"]

気軽に試す

もろもろビルド済みの Docker image を push してあるので、 pull してご利用ください。

# コンテナ外
$ docker run -i -t syucream/mruby-k2hash /bin/bash

# コンテナ内
root@9cc669af1607:/opt# ./mruby/bin/mirb
mirb - Embeddable Interactive Ruby Shell

> ... # あとはいいかんじにやる

テストとか

ちょっとだけ単体テストを書きました。テストコードを書くに際して IIJ が公開している mruby-mtest を使ってみました。 Travis CI でビルドする際はこの mgem を含めて mruby をビルドし、テストコードを bin/mruby に食わせる形でテストするようにしました。楽でいいですわゾ〜! :D

おわりに

現状の mruby-k2hash は K2HASH の多くの機能(サブキーとかキューとか)を利用できない状態なので、なんとかしたいです。 そのうち気が向いたら機能追加します。

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 を使うなども有用かも?)