こんにちは、高校 1 年生の define です。競技プログラミングが趣味で、普段は AtCoder などのコンテストに参加しています。現在は黄色コーダーですが、中 2 の時から全く進歩していません...。
さて、今年は縁あって文化祭実行委員(文実)として筑駒文化祭 2021 "EVERGREEN" の運営に携わっていたのですが、その中でも特に面白かったプロジェクト「入場管理システムの開発・運営」について記事にしようと思います。
なお、この記事の許可は取っていますが、文化祭実行委員会の公式な見解ではありません。ご了承ください。
1. 入場管理システムとは
昨年度から導入された文化祭における感染症対策の一つです。各団体の受付時にスマホなどを用いて来場者の入場証にある QR コードをスキャンしてもらうシステムで、これにより以下のような効果が期待できます。
- 各団体の混雑状況を文実がリアルタイムに把握し、対策を講じる事ができる。
- 各来場者の行動履歴を保管する事で、万が一感染者が出た際に保健所や来場者への情報提供ができる。
退場時にスキャンをしない(ゲートを除く)のは、予算や場所等の問題だったりします。おおよその滞在時刻は分かっているので、教室内の人数もアバウトですが分かるという訳です。
まぁぶっちゃけ保険です。はい。
昨年度は端末をレンタルして Android 向けアプリを動かす形で受付を行っていましたが、本年度は色々あって(後述)Web アプリになりました。ブラウザ上で動くので、例えばゲートで対応する人数が足りないような時などに容易に対応出来るのはいい所です。
ちなみに、siesta は私がプロトタイプを作っていた時に適当に付けた名前です。この頃はこのシステムが使われるとは思っていなかった(後述)のですが、使うならもうちょっとまともに付ければ良かったですね...。
由来は「探偵はもう死んでいる。」のヒロインであるシエスタですが、一応表向きにはスペイン語の「昼寝 (siesta)」から取ったという事にしておきます。
アニメは 1 話しか観てない上に、原作も 1 ページも読んでないのに付けたせいで同級生には煽られまくりました。もっとも、COCOA, MOCHA の流れを汲んで CHINO や RIZE にする案もありました(どうでもいい)。ちなみに、11 月中旬に開催される麻布文化祭で使われる入場管理システムの名前は CAPPUCCINO らしいです。
2. siesta を支える技術
※ソースコード及びシステムの URL は諸事情により非公開とさせて頂きます。今後公開する事もあるかもしれません。
この章では、siesta の仕組みについて紹介します。
siesta は Web アプリとして開発され、例によって API を通じてフロントエンドとバックエンドに分かれています。
~~セッション管理をしたくなかったので、~~認証まわりは JWT によるアクセストークンでやりました。ログインの時にトークンを発行して、以後の API 利用の時にバックに渡す形ですね。使うのは 3 日だけだしって事でトークンのリフレッシュはなしにしました。
入場処理の部分だけ見ると、こんな感じです。図が雑なのは許してください...
それでは、フロントとバックそれぞれ見て行きましょう。
2.1. フロントエンド
※途中から同期の kaage 君にフロントの開発は投げつけました。
言語は TypeScript, フレームワークは React を使いました。世はまさに大型付け時代!JavaScript マンに人権などありません。
ちなみに、ここら辺は去年の文化祭でホームページ制作をした時に身につけました。余談ですが、今年の筑駒文化祭公式ホームページもこの組み合わせです。
パッケージは QR コードの読み込みには react-qr-reader, UI には Material UI(mui), バックとの通信には axios を使いました。特筆すべき事はありませんが、Material UI の Data Grid には大変お世話になりました。
サーバはコストカットのためにバックとは分け、無料の Firebase Hosting を使いました。API を使うだけなので GitHub Pages でもなんでも動くのは良い所ですね(CSRF には注意です)。
パッケージへの文句
ちょっと文句を言うと、react-qr-reader (jsQR) の精度があまりよくありませんでした。と言うのも、iPhone のデフォルトのカメラアプリのように QR コードが多少画面と平行でなくても読み取ってくれないんですね。まぁ Web アプリの限界のような気はします。さらにもう一つ、重大な欠陥もありました(後述)。
また、Material UI の V5 がバグりにバグっててキレかけました。勝手に色が変わったりボタンが壊れたり、なんならコンテンツが表示されなかったり。リロードすれば直るんですが、結構ひどいですね。
既知のバグのようなので、早く直ってくれることを祈っています...
2.2. バックエンド
言語は新進気鋭の Rust, フレームワークは actix-web, データベースは SQLite, サーバーは AWS の EC2 を使いました。
流石は Rust, 秒間 100 件リクエストを送ってもなんなくこなしていて驚きました。API レスポンスは早かったのですが、なぜか preflight がごく稀に数秒掛かってタイムアウトみたいな事があったのは謎です。
Rust と actix-web は文化祭とは関係なく、なぜか 6 月に Web API を作ろうとして触っていました。神様のいたずらでしょうか。それにしても Rust はほとんど初めてだったので結構苦戦しました。
一方、SQLite は CTF 以外では初めてでしたが(!?)、なんなくできました。また、AWS は去年開催した Paken CTF 2020 で使っていたおかげで割とスムーズに出来て良かったです。
結局、一番面倒だったのは HTTPS 化だった気がします(笑)
3. 開発記
2 章では完成したシステムの仕組みについてお話しましたが、この章では開発中の時の事を紹介していこうと思います。
3.1. 9/28 Web アプリ化構想
元々、本年度も昨年度と同様に端末をレンタルして Android アプリを動かすはずでした。しかし、膨大なレンタル料を見て「Web アプリにして私物のスマホを使えばコストが浮くのでは?」と思い、その日の内に簡単な QR コードスキャナを作りました。
翌日には全体で議論になりましたが、文化祭における自腹禁止ルールから類推して私物利用も良くないだろうという事で、一旦消えました。
実際、文化祭まで僅か 1 ヶ月という段階で議論する事ではなかったように思います。気づくのが遅すぎましたね。
3.2. 10/2-3 プロトタイプ開発
無駄だとは分かっていましたが、なんとなく諦めがつかなかったので、適当に設計をしてフロント・バック共にプロトタイプを作りました。この時点でログイン、QR スキャン、履歴表示機能まで付いていました。先述の通り、6 月にバックだけですが似たようなものを作っていたのは大きかったと思います。
3.3. 10/13 突然の Web アプリ採用
私は何があったのか詳細は知りませんが、学校にある Chromebook 上で Web アプリを動かせばいいじゃないかという事で、Web アプリにしようと kaage に打診されました。2 週間前なんですが...
プロトタイプを作っておいた 10 日前の自分の先見の明はすごいですね。
取り敢えず私はAPI 仕様書もどきとバックを実装し、kaage には API 仕様書もどきに従ってフロントを実装してもらう事にしました。それにしても間に合う気はしませんでした。他の仕事も結構ありましたし。
3.4. 10/23 API 仕様書の作成
早くやらないと...と思い続けて 10 日間経ち、残り 1 週間を切りました。決してサボっていた訳ではなく、他の仕事が忙しかったんです(言い訳)。
kaage 一人に伝われば良いので、結局作ったとは言っても雑でした。
そしていよいよ AWS で動かそうとした時に問題が起きました。コンパイルしようとすると決まってコンパイルに失敗するのです。パッケージが足りない訳でもなく、大量のエラーと共に。さらに、今度はフリーズしてインスタンスが落ちるようになりました。
数時間格闘した後、なんとなく EC2 のインスタンスタイプをアップグレードしたら直ったので vmstat
で調べたら、コンパイルで死ぬ程 CPU とメモリを食っていました。そのせいで強制終了かフリーズしていたんですね...。
かなり時間を無駄にしました。
3.5. 10/24 実装、実装、実装
この時点でプロトタイプから実装が何も進んでいなかったので、テスト込みで 1 時間に 1 つのペースで API を実装しました。本当にけしからんですね。
まぁとにかく終わってよかったです。結局バック実装したのプロトタイプ込みで 4 日...?
良い子は真似しない!!
4. 運営記
この章では実際にシステムを文化祭で使った時の事を紹介していきます。主に反省記です。
4.1. 10/28 全日準備
この日は kaage と各団体に Chromebook を配ったり、Chromebook でちゃんと動くかを確認したりしていました。
今思うと、1 時間程度でチェックを終えず、終日動かすようにしていれば良かったです。
4.2. 10/29 Day1
当日の朝になって、ゲート用に購入した Android 端末の内 3 台で siesta が動かない事が判明しました。原因は Chrome のバージョンが古かった事で、axios が上手く動いていませんでした。チェックは入念に行うべきでした。こちらは足りなくなった一部を私物で対応してもらう事にしました。
また、貸し出した Chromebook の一部が長時間カメラを付けっぱなしにするとフリーズする事案が起きていました。これも先述の通り、長時間動かすテストをするべきでした。これについては後述します。
Chromebook が落ちた団体は私物で対応してもらう事にしましたが、中 1 はスマホ禁止だったのでどうしようもなく、紙で書いてもらう事にしました。直接 ID を入力する方法も用意すれば良かったと思います。
4.3. 10/30 Day2
私物利用を全面的に許可したのでトラブルは少なくなりました。
中夜祭関連で人為的問題は起きていましたが、技術的な問題はなかったように思います。
4.4. 10/31 Day3
1 秒に 1 人の早さでゲートでの処理が行われていた時があって驚きました。SCC (Security, Counting, Cleaning) 委員の皆さん凄すぎ。
最終日なのもあって、特に目立ったトラブルはなかったです。終わった後に紙で書かれたものが提出された時にはため息をつきましたが...。
5. 不具合について
5.1. 不具合の中身
4 章でも書いた通り、siesta を動かしている時に貸出中の Chromebook がフリーズする不具合が散見されました。
また、スマホでも長時間カメラを付けているとフリーズこそしないものの、カメラは動いているが QR コードの読み込みは出来ないという状態に陥る事がありました(リロードで直る)。
結論から言うと、これはブラウザにおける Worker の Garbage Collection (GC) に関する問題で、Worker を作り直さない限り GC がいつ発動するか不明、そしてそれが上手く発動しないとメモリリークが起きるようでした。さらに、GC のアルゴリズムはブラウザのバージョンによって異なり、バージョンによっては GC が上手く動きませんでした。
例えば、Chrome のバージョン 91.0.4472.164 では動いていましたが 95.0.4638.69 では追いついていませんでした。しかし、Chrome の中身であるはずの Chromium では 95.0.4638.69 でも動いていて謎です。
ちなみに、他のブラウザ、例えば Firefox では最新版で十分 GC が動いていました。
react-qr-reader の中身が 0.5 秒に 1 度 canvas から画像データを取って Worker で動いている jsQR に渡すという仕組みだったのですが、この画像データのメモリが解放されないためにメモリリークが起きたという事です。
本当にやっかいなバグを踏んでしまいました。
当該 Issue
Issue では Worker を定期的に作り直す方法で一時的に解決していますが、本質的な解決にはなっていません。
5.2. なぜ不具合を防げなかったのか
このように厄介過ぎるバグを踏んでしまった訳ですが、今考えると未然に防ぐ/すぐに止める事は出来たのではないかと思います。では、なぜ止められなかったのでしょうか。
原因 1. 実機試験が甘かった
Web アプリとして開発した上、QR スキャンのプログラムはほぼサンプルと変わらない単純なものであったので、自分の PC で上手く動いていればパフォーマンス以外で機器によって問題が生じる事はないだろうと考え、実機の Chromebook での試験を短めに終わらせていました。ここで 1 時間でも連続した試験を行っていれば不具合には気づいていたかもしれません。
まさかバージョンによってバグが出るとは思ってもみなかったのです。
原因 2. メモリリークに気づかなかった
実際に問題が生じた台を回収して検証を行う際に、一時はメモリリークを疑い Chrome の Devtools からメモリ使用量を確認したのですが至って普通でした。なぜかと言えば、Total Heap Size に Worker のメモリは含まれていなかったからです。そのため、初期からメモリリークの可能性を排除してしまっていました。
後日 Chrome の Task Manager で確認して初めてメモリリークに気づきました。
また、Chromebook の代わりに私物スマホでやってもらうようにした所、不具合はせいぜいリロードで直る程度で、フリーズするようなものではなかった事で、単に Chromebook の性能が悪いのが原因だと思っていました。
原因 3. パッケージの Issue を確認していなかった
手元でちゃんと動いていたので、Issue を全く確認していませんでした。
原因 4. ブラウザのバージョンを最新にしていなかった
非本質ではありますが、私が Chrome を最新版にしていれば自分の PC でも気づいていたでしょう。
それにしてもあまりに想定外な不具合だった上に時間もなかったので、最善を尽くしたとしても不具合を完全に防ぐ事はできなかったかもしれません。
しかし、影響の規模から言っても不具合を防げなかった責任は大きいですし、少しでも早く気づいていればと思うと悔やんでも悔やみきれません。この反省は今後の開発に活かしていかなければいけないと強く思います。
6. おわりに
3 日間通して協力してくださったご来場者の皆様、各団体の受付担当者、そしてそれ以前から運営に携わった委員の皆さんや先生方には深く感謝しています。特に、フロントの開発で仕様を変更したり色々要求したりしても忙しい中付き合ってくれた kaage には頭が上がりません。
一方で、不具合によってご迷惑をお掛けした団体の皆さんには大変申し訳ないですし、紙ベースでのデータが残っているとは言え欠損データが出てしまった事は追跡上重大です。改めて、今後は今回の反省点を活かしていく所存です。
バックで問題が起きなかったのはせめてもの救いですね...。
最後に、来年は入場管理システムなしに自由に文化祭が出来ている事を願って記事を締めくくろうと思います。
そして、最後の最後に。たんもし最高!