こんにちは! traP 代表のたけのひとです。この記事は ひとりブログリレーの2本目です!
…これ、夏に出す予定だったものです。ブログリレー落とした後にブログを完成させてたのですが、出すタイミングを見失ってました。ごめんなさい。
------
8月にサイバーエージェントさん主催の CYCOMPE というコンペイベントに参加してきたので、その参加記を書いておこうと思います!
イベントの概要
CYCOMPE のホームページより。
CYCOMPE は架空のアプリケーションのチューニングコンペティションです。
サイバーエージェントが開発において大切にしている意図やポイントを落とし込んだ開発現場目線の問題にチャレンジできます。
事前情報は本当にこれだけで、イベントとしては仕様書が渡されるので、それに沿ったアプリケーションを作ろうという感じのものです。空の(正確には採点用途のための Dockerfile と GitHub Actions だけの)リポジトリを渡されて、2 日間で 0 からプログラムを組みます。そして、仕様書は途中でいきなり増えていく…というイベントです。main ブランチにコミットする度に機械採点が行われ点数が表示され、それと最後に社員による採点(アセスメントスコア)が点いて順位が付きます。
イベント前日まで(事前準備)
せっかくの機会なので何か準備しようと思ったのですが、事前情報が本当になにもなく、チューニングコンペティションということだったので pprotein の使い方だけ勉強しようと思って ISUCON を軽く走りました。
手元の環境で docker compose で走らせようと思ったのですが、Slow Query Log が上手く出てこなくて nginx も Docker 上で動いてるために設定をいじるのが面倒で、結局あきらめました。
結果的に全くもって無意味な練習でしたが、環境構築の勉強はできたのでギリワース。
イベントスタート
イベントはオフラインとオンラインのハイブリット形式だったので、会場に居たのは 10 人弱くらい。バックエンドはイベント全体にシナリオみたいなのがあって、それをもとに進んでいきます。イベント開始後、架空の会社「ドカンと解決下山ソリューション」の社長が現れ、ABEMA TV が羨ましいので自分も「あれまぁ TV」を作りたいと言い出し、その人のやりたいことを基に仕様書が渡されて、それの開発をしてください、というスタート。
「ドカンと解決下山ソリューション」の社長さん、黒い変なメガネとネックレスみたいなのをつけててインパクト強くて面白かったです。見た目から無理難題を押し付けてくるかと思いきや、イベントを通して内容は割とまともな機能追加だけで、見た目で人を判断しちゃいけないなと思いました。ごめんなさい。
あとから発覚したのですが、社長役の人は普通にマネージャー職の偉い人でした。てっきり営業職の人を連れてきたと思っていたのでびっくり。
開発の流れ
コンペ中で、一番びっくりしたのは渡されたリポジトリが空っぽだったこと。2 日間で限られてるし、チューニングコンペと書いてあったので最初から仮実装が与えられると思ってました。1 から実装だったので大変でした。
master に一度プッシュすると、自動で採点が走ります。
lint でエラー項目が出ていると減点され、API の動作が誤っていると減点されます。そしてテストのカバレッジが高いと加点され、最終結果が Slack に表示される、という流れです。
採点結果が Slack の個人チャンネルに流れてくるのですが、このチャンネルそれ以外では使ってなかったのでせっかくだしと思って自分の times にしちゃいました。他の学生に見えるチャンネルじゃないので、便利ですね。
今考えたら当たり前なんですけど、ここのチャンネルは運営の社員さんが普通に見てたらしく、裏で times にしてる人として認知されててちょっと恥ずかしかったです。
考えをテキストにした方が考えやすいので、普段趣味で開発してるときはよく traQ の times に書き込んでたりします。見返せるし楽なんですよね。バイト先も times できないかな…。
ファイル構成
ここ 1 年で DI の理解はかなり深まったといっても、趣味の開発では DI を使っておらず、開発速度を求められるイベントで使うのはちょっと怖かったので DI を使わずで実装しました。層の数も少なめに Controller っぽい層と Model 2 層での開発になりました。ビジネスロジックとデータ保存を Model で行ってます。
ちなみに今回はインメモリで保存してください(永続化は特に不要)というルールだったので、Model 層にデータを保存する Map 構造体を置いて管理する戦略を取っています。
カバレッジとのたたかい
1日目は割と余裕があって、途中からテストコードのカバレッジを上げることにしました。ただ途中から夢中になりすぎて、新しい仕様書のことをそっちのけでカバレッジ上げをしようとして、v2 がエラーを出していて減点されていたのに気づかないまま 1 日目を終えます。やらかした。1日目の結果で足切りがあったので、正直ずっと心配でした。あとはテストを今までしっかり書いてこなかったので、普通にテストの書き方覚えてなくて苦戦しました。
2日目:決済 API との格闘
2 日目のスタート時には新たにプレミアム会員の決済についての仕様が追加されました。
決済周りは自分が開発しているものとは別のアプリケーションを API 経由で使用する、という形になっていて(実際クレカ情報とかは自社で保存せずに別になってたりするもんね)、自分で API のラッパーを作成することに。ここまで来ると API の仕様書も割と十分に複雑なところまで来ていて、若干パンク気味になりながら開発を進めていました。
コンペ中の最大のやらかし
そのラッパーを作成していた際にできてしまって、最後まで解消できなかったバグがあります。以下そのコード、のちょっといじったやつです(コードを直接公開するのは NG だったので、少しだけぼかして書いてます)
GET リクエストをあるところに飛ばすというコードです。さてどこが悪かったでしょう?
func GetItem(userId string, limit *int64, offset *int64) (Response, error) {
uri := "items/?userId=" + userId
if limit != nil {
uri += "&limit=" + strconv.FormatInt(*limit, 10)
}
if offset != nil {
uri += "&offset=" + strconv.FormatInt(*limit, 10)
}
var res Response
err := getRequestJSON(uri, &res)
return res, err
}
(ちなみになぜ getRequestJSON なんてものが存在しているかというと、決済APIへのリクエストの最初の数回はかならず落ちるというとんでもない仕様があってこうなっています)
2行目、URI を組み立てる際に userId のエスケープをしてません。無意識に userId は良い ID が来ると仮定していたのですが、実際はそんなことはなく(テストケースの生成でスペースが入る文章みたいなのが入ってたっぽい)、それでコードがバグって落ちていました。うーーーん、まぁでも僕が悪い。
uri := "/?userId=" + url.QueryEscape(userId)
こうするのが正解でした。エスケープって結構簡単にできるんですね。
終了 5 分前にその可能性には気づいたのですが、そのバグが原因だと信じきれずとあと時間がないために直せずでした…。
言い訳マンション
めっっっちゃ言い訳するんですけど、自動で入っているテストがどういうデータなのかみたいなのを確認する手段がなくて、採点は採点環境上で Docker がビルドされてそれで実行で、コンテナのログとかも確認できなかったので開発がむずかったです。fmt.Println とかで userId 全部吐き出したら気づけたはず!!でも皆同じ環境なので自分が悪いです…。
ところで↑に関連してですが、参加者の一人はアプリケーションに Sentry を仕込んでエラーを監視できる環境を作ったらしいです。天才すぎる。その発想はなかった。次回は禁止されそう。
結果
4位でした。3位から入賞だったので惜しい…!自分が 103 万点で 3 位が 107 万点だったので結構僅差でした。↑のバグが原因で入賞逃したと思ってたんですけど、今直してテスト走らせても 5000 点しか上がらず…。でもこれにかなりの時間を吸ってしまっていたので、実際このバグを踏まなかったらもう少し点数出せてたと思います。実はそんなことなかったかもしれないけど。
スコアリングについて
せっかくなのでスコアの計算方法とそれに対する感想を書いておきます!
※公式にはスコアの計算方法は非公開なので、あくまで勝手な推測です!
基本的には「API をどれくらい仕様書どおりに満たせているか」「何個 lint で落ちているか」「テストのカバレッジがどれくらい高いか」の3要素から計算される機械的な採点スコアが Max 100 万点で、それと別に運営による採点スコアが、多分 Max 20 万点の合計 120 万点での勝負でした。
結構くせがあったのは、「テストのカバレッジがどれくらい高いか」で、このシステムに終始苦しめられちゃったところがあります。そもそもカバレッジを 100 % にするのが善かと言われるとそうではないので、点数を上げるためだけに書いてる、みたいな気分になってました。まぁでも自分が運営するってなったら、カバレッジの高さは点数指標に入れたくなる気持ちもわからないでもない…?テスト点が機械採点の 15 % を占めていたイメージです。
それと、機械採点でたしか自分は 90 万点くらいあった(上位陣だと10万点くらいしか差がつかなかった)のに対して、最後運営のコードレビューが Max で 20 万点足されていたので、そこで勝敗がついてしまっていた感じがあって何が敗因だったかちょっとわからかったのがもやもや。わりかし丁寧なコードを書いたつもりだったけど……(まぁでも拡張性があるかと言われると…)。運営にコードのフィードバックほしいです!って投げたので、それの回答待ちです。
ただそもそも満点近い勝負になることを運営があまり想定していなかったのもあるっぽく(あんま実情わかんないけど)。自分もコンペじゃないけど大会の運営は何度もやったことがあるし、参加者側に立って、改めて運営ってすごく難しいなと思いました。自分がコンペを設計するとしたらどうしてたかな…?
というか、こういうのは負けた側がシステムに微妙な顔をするもので、多分勝ってたら何も考えず俺の勝ち〜とか言ってたと思うし、4位は4位なので次回参加できたらそのときにリベンジします!!絶対次回はもっと良くなってると思うし!
感想
入賞を逃しちゃったのは普通に悔しいけど、楽しかったです。2日間もの間、朝から夜までハッカソン以上のスピードで実装していて、一瞬も暇な時間がなかったので割と大変でしたが、久しぶりに Go 書けたし満足です。でももっとカオスなものが出てくると思っていたので、少し物足りなかったところも…(バグ残してたのに何を言うんだという感じだけども!!)。こういうコンペイベントみたいなの、もう少し探して出てみようと思います。
ブログリレー、次の担当は @Takeno_hito です。大会の話 その2 を予定してます!(明日出せるかわからん…)