この記事は、ひとり開発 Advent Calendar 2019の2日目の記事になります。
1日目の記事はhimataniさんのひとり開発でも諦めない、これからのプロダクトのつくり方でした。
はじめに
こんにちは、ぷらす (@p1ass)です。
皆さんは個人開発をする時にしっかりとテストやデバッグをしていると言い切れますか?
私は趣味レベルで開発しているときはスピード優先でテストをないがしろにしていまうときがあります。しかし、大抵バグを埋め込んでしまい、後々大変なことになってしまいます。
この記事では、私が個人で開発しているMemoito(めもいと)を開発・運用していく上で見つかったバグ・不具合を自戒を込めて一挙紹介していきます。
この記事を読んで、今一度、自分の開発しているサービスを見直すきっかけになれば幸いです。
Memoitoとは
最初に、この記事で紹介していくバグが発見されたWebサービスであるMemoitoについて先に紹介しておきます。
MemoitoはTwitter連携を用いて、メモをフォローしている人と紐付けて保存することができるWebサービスです。
Twitterで気軽に連絡先を交換できるようになりましたが、徐々にフォローしている人が増えてきて、**「この人誰だっけ?」**となる経験はないでしょうか?
Memoitoはフォローしている人と紐付けてメモを保存できるので、スマホの標準メモアプリでメモするより簡単にメモを取ることができます。
勉強会やミートアップ、カンファレンス等で会った人のことを、その時話した内容と一緒にメモを取れば、後から見直すことができてとても便利です。
公式Twitterもあります。
Memoito(めもいと)はSNSの繋がりと紐付けてメモを保存できるWebサービスです!
— Memoito(めもいと)公式 (@MemoitoOfficial) September 30, 2019
フォローした理由をメモしておいたり、オフ会で会った時に話した内容をメモしたりできるので是非使ってみてください!https://t.co/r7rzQ9sXI4
アーキテクチャ
サービスのアーキテクチャが分かっていた方が記事を読みやすいと思うので、軽く説明します。詳しいことは、今度書く予定の記事を参照してもらうとして、こんな感じのアーキテクチャになっています。
フロントエンド
- Nuxt.js
- Netlify
バックエンド
- Go
インフラ
- GKE
- CloudSQL
デプロイはCircleCIに寄せていて、Dockerイメージのpushやkubectl apply
などを実行しています。また、GCPのインフラ構成定義にはTerraformを使っています。
個人ではオーバーエンジニアリングですが、頑張って今風なアーキテクチャにしています。
見つかったバグ
さて、ここからは見つかったバグを紹介していきます。
リリース前の検証ユーザがDBに残っている
問題
リリース前に無効なIDを持つTwitterのユーザ情報をインサートしてしまっていて、それがDBに残ったままでした。
mysql> select id from twitter_users where id = 0;
+----+
| id |
+----+
| 0 |
+----+
1 row in set (0.04 sec)
ここでのid
はTwitter側の内部IDと同じ値なので、0になることはありません。
おそらく、ゼロ値の構造体をそのままインサートしてしまっていたが原因と考えられます。
対応
直接本番DBのコンソールからrowを消しました。
ただ、この作業はさくっとSQLを1回発行するだけではダメでした。
SQLの制約がいくつか張られていたので、他のテーブルのrowを先に削除する必要がありました。
間違って他のユーザのデータを消すわけにはいかないので、入念に調査をして、冪等性があるクエリでrowを削除するようにしました。
無効な値を持つレコードがDBに格納されている
問題
これも上と似ていますが、本来存在し得ない値が書き込まれていました。
たとえ話ですが、ある状態を表すカラムstatus
は本来、[0, 2]の値を取るはずが、3が書き込まれている、といった状況でした。
これは、JSON APIでPOSTされた値のバリデーションが漏れていたのが原因でした。
対応
全てのフィールドに対して、値が有効であるかを確認するバリデーション関数を実装し、テストもしっかりと書きました。
いくらクライアントサイドでバリデーションしているとはいえ、サーバ側でも実装しなきゃなという気持ちになりました(当たり前)。
1対1で対応しているはずのテーブルのレコード数が違う
問題
Memoitoは、将来的に他のSNSの対応を見据えているため、ユーザ情報は汎用的なusers
テーブルとTwitter固有の情報を格納しているtwitter_users
テーブルに分けて保存しています。
現時点では、Twitterしか対応していないため、users
テーブルとtwitter_users
テーブルのrowの数は一致するはずですが、なぜか一致していませんでした。
mysql> select count(id) from users;
+-----------+
| count(id) |
+-----------+
| 200 |
+-----------+
1 row in set (0.05 sec)
mysql> select count(id) from twitter_users;
+-----------+
| count(id) |
+-----------+
| 184 |
+-----------+
1 row in set (0.04 sec)
これは、トランザクションが正しく貼れていなかったことが原因で、片方が失敗した場合に不整合が生じていました。
対応
まず、2つのテーブルに対するインサートを同じトランザクションで実行するようにアプリケーションを修正しました。
その後、おかしいrowを探し出し、SQLでパッチを当てていきました。
具体的には、
- 不必要な
users
のrowを探す - その
id
が使われているメモ(notes
テーブル)を探す -
notes
のuser_id
を正しいものに変更する - 参照が全てなくなった不必要な
users
のrowを削除
という手順で対応しました。
これが、一番修正が面倒くさいバグでした。
おかしなrowの洗い出しが大変でしたし、SQLの制約を考慮しつつパッチを当てなくてないけないのでなかなか骨の折れる作業でした😇
Twitterのプロフィールが更新されない (未解決)
問題
MemoitoではTwitterのプロフィールをこちら側のDBに保存しています。
登録ユーザはログインするたびに新しいプロフィールに更新されるようになっています。
しかし、メモを取った相手は必ずしも登録ユーザではないため、プロフィールが更新されていませんでした。
結果として、プロフィールアイコンが404で表示されないという不具合がありました。
対応
実はこの不具合はまだ修正できていません。せっせと実装中です。
DBに保存されているユーザの全てのプロフィール情報をTwitter APIから取得して、プロフィール情報を更新する方法を考えています。
おそらく、一番オーソドックスな方法なのではないでしょうか。他に良い方法があれば教えて下さい。
おわりに
ここまで、リリースしてから見つかったバグを紹介してきました。
特に整合性周りがきちんと実装していないと、不具合が生まれがちなので次からはしっかりと考えて実装しようと思いました。(対応大変なので、、、)
皆さんもこの記事を反面教師として、今一度自分のアプリケーションを見直してみましょう。何かヤバいものが見つかるかもしれません、
最後になりますが、よかったらMemoito使ってみてください!
Memoito(めもいと)はSNSの繋がりと紐付けてメモを保存できるWebサービスです!
— Memoito(めもいと)公式 (@MemoitoOfficial) September 30, 2019
フォローした理由をメモしておいたり、オフ会で会った時に話した内容をメモしたりできるので是非使ってみてください!https://t.co/r7rzQ9sXI4
明日はひとり開発 Advent Calendar 2019はbinnmtiさんの担当です。お楽しみに。