ISUCON4予選問題をngx_mrubyだけで解いてみた(ノウハウ編)

  • 11
    Like
  • 4
    Comment
More than 1 year has passed since last update.

======

この記事はmod_mruby ngx_mruby Advent Calendar 2014の5日目の記事です(というか、2日目の記事の続きです)

昨日はhfmさんのngx_mruby(memcached付)をCentOS/SLでビルドする時のハマりどころと直し方についてでした。

今回は、ISUCON4予選問題をngx_mrubyだけで解いてみて分かった、細かいTipsやハマりどころ等を書きたいと思います。
Tips系ということで、2日目はブログに書きましたが、今回はQiitaにしてみました。

Tips

色々なリクエスト情報の取り方

GET

Nginxの組み込み変数である$argsから取れます。

example1.rb
r = Nginx::Request.new
Nginx.echo r.args #=> "id=hoge&name=fuga"

ちなみにargsだと先頭の?が付かなくて、query_stringだと先頭の?が付きます。
なので、argsの方が若干扱いやすいかと思います。

2014/12/07追記:

ngx_mruby作者のmatsumotoryさんから補足を頂きました。
以下のようにarg_[key名]でkeyを指定して取れるそうです。
断然コチラのほうが扱いやすいですね!

v = Nginx::Var.new

v.arg_name #=> "matsumotory"
v.arg_id #=> 0

POST

これも同じように$request_bodyから取れる…と思いきや取れません。
ちょっと、どこに書いてあったか忘れたのですが、$request_bodyが有効になるのはかなり後のフェーズらしく、Nginx自体がそのフェーズにフックできるようになっていないとかで標準では無理なんだそうな…
ログ出力は当然最後なので、ログに書き出すことは標準でもできますが、$request_bodyを見てrewriteするとかはngx_mruby関係なくできません。

じゃあ、無理なの?っていうと、まあ一応やり方はありました。
だいぶダーティハックっぽいのですが、form-input-nginx-moduleなるNginxのサードパティモジュールを入れて、それを使おうとすると、$request_bodyが扱えるようになります。

nginx.conf
set_form_input $login;
set_form_input $password;

本来はこのような記述をすることで、$request_bodyからそれぞれ変数名に対応したkeyに対するvalueを変数に格納してくれるような挙動をするのですが、それと同時に$request_bodyを弄るモジュールの性質上、他からも$request_bodyが参照可能となります。

example2.rb
r = Nginx::Request.new
Nginx.echo r.var.request_body #=> "login=hoge&password=fuga"

とまあ、かなりのダーティハックです。
この辺は他に良い方法をご存知な方がいらっしゃれば是非、補足いただけると嬉しいです。

Cookie

これについては、方法は2種類(?)あります。

  • Nginxの組み込み変数$http_cookieから取る方法
example3.rb
r = Nginx::Request.new
Nginx.echo r.var.http_cookie #=> "login=hoge&password=fuga"
  • リクエストヘッダから取る方法
example4.rb
r = Nginx::Request.new
Nginx.echo r.headers_in['Cookie'] #=> "login=hoge; notice=fuga"

Cookieを操作する

Cookieのセット

レスポンスヘッダのSet-Cookieにセットします。

example4.rb
r = Nginx::Request.new
r.headers_out['Set-Cookie'] = "login=hoge; path=/"

Cookieの削除

Cookieを削除させることはできませんので、Cookieの有効期限を過去にすることで無効にさせます。

example4.rb
year = Time.now.year - 1

r = Nginx::Request.new
r.headers_out['Set-Cookie'] = "notice=+; expires=Mon, 25-Nov-#{year} 11:11:11 GMT; path=/"

遭遇したハマりどころ

デフォルトではPOSTが取れない

対処法(ダーティハック)は上記を参照ください。
これはngx_mrubyというよりもNginxの制約ではありますが…

Rubyの組み込みクラスと同じ名前であっても、メソッドも同じとは限らない

遭遇したのはTimeクラスでした。strftimeメソッドが無かったです。

Rubyならこう書く所を

example5.rb
p Time.now.strftime("%Y-%m-%d %H:%M:%S") #=> "2014-12-05 00:11:22" 

こんな風に書いてみたり…w

example6.rb
year  = Time.now.year
month = Time.now.month
day   = Time.now.day
hour  = Time.now.hour
min   = Time.now.min
sec   = Time.now.sec

month = month < 10 ? "0#{month.to_s}" : month.to_s
day   = day   < 10 ? "0#{day.to_s}"   : day.to_s
hour  = hour  < 10 ? "0#{hour.to_s}"  : hour.to_s
min   = min   < 10 ? "0#{min.to_s}"   : min.to_s
sec   = sec   < 10 ? "0#{sec.to_s}"   : sec.to_s

Nginx.echo "#{year}-#{month}-#{day} #{hour}:#{min}:#{sec}" #=> "2014-12-05 00:11:22"

まあ、これもngx_mrubyというよりはmrubyのハマりどころですね…

あとは、2日目に書いたのですが、原因はまだ切り分けてませんが、Nginx.redirectが不正な形式のレスポンスを返しちゃうことですね。

とりあえずは、302リダイレクト時のレスポンスを模倣したレスポンスを返す形で以下のようにしました。

example7.rb
r = Nginx::Request.new
r.headers_out["Location"] = "http://#{r.var.http_host}/mypage"
Nginx.return Nginx::HTTP_MOVED_TEMPORARILY

こちらからは以上です。
明日はかっば(inokappa)さんのmod_mruby と mruby-aws-s3 を使って S3 上の HTML をコンテンツを表示させてみようなります!