ISUCON11 予選おつかれさまでした。
ここ数年は参加者として予選敗退を繰り返してきたのだけど、今年はちょっと違う関わり方をしてみるか、と思い 「参考実装の移植」に立候補してみました。
Node.js担当として採用していただき、ちょっと不安もあったので id:hokaccha 氏にレビュアーとしてついてもらって、言語移植チームとして加わりました。
ISUCON11予選おつかれさまでした。今回は言語移植チームとしてNode.js実装を担当し、その他 バグ直し太郎として幾つかの言語の実装にcontributeしました
— すぎゃーん💯 (@sugyan) August 22, 2021
Node.js 実装
中身としては素朴な express
のアプリケーションで、TypeScriptで実装しました。 mysql clientには mysql2/promise
を使うことで async/await で簡潔に書けそうだったのでそれでやってみました。
行数としては元実装の Go 1262行に対し 1243行とほぼ同等、それなりに丁寧に型定義などを書きつつ 元実装を忠実に再現したものになったかと思います。
昨年のisucon10のリポジトリなどをとても参考にしつつ、今回の自分の実装が未来の実装者の参考にもなるように、と気をつけて書いたつもりです。
1組だけですがNode.js実装で本選にも残ったチームも居たようで良かったです。
開発環境
移植作業をする時点で既にGoの参考実装やbenchmarker、そして開発環境がある程度できあがっていて、作業を進めやすくてとても助かりました。このへんは作問チームのレベルの高さをすごく感じました。
複数のコンポーネントからなるそれなりに複雑なアプリケーションながらも、docker-compose.yml や Makefile がしっかり用意されていて、そこに自分の実装する言語用の環境を用意できればコマンド一発でWebアプリが起動できるし benchmarkerを走らせたりもできる。
benchmarkerは -no-load
オプションで負荷走行前のアプリケーション互換性チェックシナリオを走らせるので、まずはそこがすべて通るようになれば大体の移植は出来ていると判断できる、という具合。
勿論CIでもテストが走るようになっていて、それらが通っていてさらに作問チームメンバーやアドバイザリーからレビューを受けた上でapproveされたらmergeできる、といったルールもしっかり整備されていて、とても体験の良い開発環境でした。
実装についての疑問や相談もSlack上でいつでも作問メンバーが素早く回答してくれて、とにかく素晴らしいチームだな、と思いました。 このチームの人たちと同じプロジェクトに取り組めたというだけで 今回応募してみて本当に良かったと思っています。
Contributions
Node.jsの実装も一通り出来たところで 他の言語実装も出揃ってきていたので試しに動かしてみて、細かいエラーが出ていたところを修正したりもしました。
「オレでなきゃ見逃しちゃうね」的な細かいものとか
- fix(python): Fix generate_isu_graph_response by sugyan · Pull Request #1361 · isucon/isucon11-qualify · GitHub
- fix(ruby): Fix valid_condition_format? by sugyan · Pull Request #1378 · isucon/isucon11-qualify · GitHub
あとはPerl書ける人が少なかったようなので「いちおう私も多少Perlの経験あるのでレビューくらいなら」とちょっと見てみたり
benchmarkerの細かい挙動について指摘したりライブラリのバグをついたりもしました
- [bench] 偽装したJWTが壊れている · Issue #1179 · isucon/isucon11-qualify · GitHub
- [bench] unicode escapeされたJSONを返すとERRになる · Issue #1300 · isucon/isucon11-qualify · GitHub
あと実際に本番前々日くらいにサーバ上で各言語の初期実装に対してbenchmarkしてみたところ、何故かNode.jsがGo実装よりも2倍以上高いスコアが出た(!)という謎現象が見つかり、調査の結果それはNodeがたまたまbenchmarkerの秘孔をつく動作をしていたことが分かり修正されたのですが、そういうのを見つけることが出来るという点でも複数言語での移植は意義があるのだなぁと思いました。
tagomorisバグ
余談。
Perlの実装を手伝っているときに、benchmarkerを走らせていると何故か予期せぬところで 401
を返していてチェックが失敗するという現象が起きていた。
401
ってことはcookie-sessionまわりだよな〜 でも特に変なところとか無いはずだしな〜 と id:kfly8 氏と一緒に見ていて、ところで Plack::Middleware::Session::Cookie
の secret
は "tagomoris"
とかじゃなくて 今回は "isucondition"
(もしくは SESSION_KEY
環境変数) で統一するって話じゃないですかと指摘してそこだけ直してもらった
builder { enable 'ReverseProxy'; enable 'Session::Cookie', - session_key => $ENV{SESSION_KEY} // 'isucondition_perl', + session_key => 'isucondition_perl', expires => 3600, - secret => 'tagomoris'; + secret => $ENV{SESSION_KEY} || 'isucondition', enable 'Static', path => qr!^/assets/!, root => $root_dir . '/../public/';
この変更を入れたら 先述の 401
のエラーが出なくなり…。
「えっ cookie の secret key を "tagomoris"
から違う文字列に変えただけでバグが直ったの!?」と混乱した、という出来事。
種明かしをするとこの変更は secret
指定の末尾が ;
だったのを ,
に変えてしまっているというミスが含まれていて。
-MO=Deparse
してみると 変更前は
&builder(sub { enable('ReverseProxy'); enable('Session::Cookie', 'session_key', 'isucondition_perl', 'expires', 3600, 'secret', 'tagomoris'); enable('Static', 'path', qr"^/assets/", 'root', $root_dir . '/../public/'); $app; }
というものだったのが変更後は
&builder(sub { enable('ReverseProxy'); enable('Session::Cookie', 'session_key', 'isucondition_perl', 'expires', 3600, 'secret', 'isucondition', enable('Static', 'path', qr"^/assets/", 'root', $root_dir . '/../public/')); $app; }
と解釈されてしまう。続く Static
middleware を enable
した結果が Session::Cookie
の引数に並べられる形になり、内部ではその値は無視されるのだろうけど、何が起こっているかというと「enable
が呼ばれる順番が変わる」。
ここでまた別の事象として、benchmarkerが「/assets/
以下に含まれるファイルをGETした際に Set-Cookie
ヘッダが含まれているとそれによってbenchmark scenarioを回しているagentが別人になってしまい その後のリクエストで 401
になってしまう」というバグがあった。
つまり このpsgiでは 先に Session::Cookie
を enable
していると その後に呼ばれる Static
のファイルたちにも Set-Cookie
がつくことになり、そのbenchmarkerのバグをつくことになってしまっていた。 "tagomoris"
を修正した際に間違えて ;
を ,
に変えてしまたことにより意図せず Static
→ Session::Cookie
の順に enable
する形に変わっていて そのバグをつかないように変更されたので エラーに遭遇しなくなった、というオチでした。
奇妙な挙動にとても戸惑ったけど 複数の不具合が密接に絡んで起きた奇跡のような現象でした。という話。
いやー数年ぶりに書いたけどやっぱりPerlむずかしい…。 一晩で解決できたのも奇跡だったのかもしれない
本選へ
…というわけで ともかく予選の移植は無事(?)に完遂し、本番でも特に言語実装依存の不具合なく終えられたようで何よりでした。
また本選に向けて頑張っていこうと思います。
よろしくお願いします。