【教訓】Mastodonを改造するならテストコードを書こう

あすかです。

Fediverse Advent Calendar 2023の16日目の記事です。

Mastodonの存在は以前から知っていましたが、今年1月あたりからTwitterの規制とかに不満があってPawooに行っていました。
今年2月、Pawooのサーバーが大変不安定な状況が続いていまして、その自分用の避難所としてMastodonサーバーを建てました。

建ててみて思ったんですけど、誰も来ないんですよ。まぢで誰も来ない。

ネットの知り合いがいたんですけど、PawooとかjpとかMisskey.cfとかいっちゃってなかなかこっちに登録してくれないの。1人しかいないサーバーって魅力はこしあんおはぎ未満なんだなと。でも赤福はおいしいよと。そんな状態でした。
2月14日にDiscordで宣伝してみたら一気に人がたくさん来て鯖落ちしたのであわててサーバーのスペック増強しまくったらオーバースペックの3年契約になって今に至ります。


kmyblueはちょっと改造してます

さてそんな赤福よりも魅力のないうちのサーバーですが、ちょっといくつか独自改造を加えています。

2月11日にサーバーを建てて、Reactはちょっとだけ分かるけどRubyの経験ゼロまったくの初心者の状態で最初に作ったのが絵文字リアクションでした。

最初は1つの投稿に10個つけられましたが、そのあと5個に減らして、今は3個になっています。

これをきっかけに、ローカル公開、Markdown、時限投稿など、いくつかの改造を加えるようになりました。

Mastodonに改造を加えることによる本家追従問題

最初はMastodon本家の仕様変更に追従する意味でも改造は絵文字リアクション、ローカル公開ぽっきりにするつもりでしたが、あれもこれもといろいろ足しちゃいまして、ついに本家とのマージで競合が起きるようになりました。

Mastodonサーバーに改造を加えることで、本家の変更やリファクタリングへの対応が難しくなる問題。実はMastodonフォークいくつかありますけれど、他のフォークも同じような問題に直面しているようです。

例えばglitch-socというMastodonフォークでは、バージョン4.3.0 betaとなっていまして、この記事執筆時点の最新バージョンまで追従しています。

https://github.com/glitch-soc/mastodon/blob/main/lib/mastodon/version.rb

ですがHometown(これもMastodonフォーク)では、4.1.0 rc1になっています。4.1.0ですらない、その一歩前のrc1という状態。

https://github.com/hometown-fork/hometown/blob/hometown-dev/lib/mastodon/version.rb

Fedibirdなんかは4ですらない、3.4.1になっています。

https://github.com/fedibird/mastodon/blob/fedibird/lib/mastodon/version.rb

ここで3つのMastodonフォークを挙げましたけど、本家に追従しているのとしてないのとに分かれています。
じゃあここで問題ですけど、追従しないメリットってどこにあるんでしょうか。

  • 古いバージョンのデザインや設計の長所を活かし、最新のMastodonと差別化を図れる

  • 最新のMastodonのリファクタリングについていく必要がなく、デグレードなどもなく維持が楽。コストが低い

  • 本家のリファクタリングを気にせず作れるので、コードを自分好みにリファクタリングできる。自分にとって保守しやすくなる

反対に追従しないことによるデメリットというのもありまして、

  • 本家の最新機能が利用できない。本家のコードから必要な部分を選んでマージするか、自分で作るしかなくなる

  • 本家のアップデートによるバグ修正、セキュリティパッチがそのままでは適用できない

  • 本家が依存するライブラリのセキュリティ修正パッチやアップデート/Ruby言語の新機能などが使えない

  • そもそも本家や依存パッケージのサポート期限がある(例えばMastodon 4.1.0が使用しているRails 6.1のサポートは2023年末までです)

セキュリティも考慮するなら追従した方がいいと、kmyblueでは判断しました。

本家に追従することによるリスク

セキュリティのために追従しました。

で、本家に追従することで何が起こったかというと、今年7月29日にkmyblueでこういう障害が起こりました。

これ何が起きたかというと、Mastodonでは投稿の公開範囲いくつか選べますよね。今はサークル投稿もあるんですけど、当時は「公開」「ローカル公開」「ログインユーザーのみ」「フォロワーのみ」「指定された相手のみ」の5種類。
「ログインユーザーのみ」を除く4種類の公開範囲が全部、誰でも簡単に見れる状態になっていました。(画像の内容とは矛盾してますが後日訂正してます)

「フォロワーのみ」はフォロワー以外に見せたくない投稿、「指定された相手のみ」は遅くても4.1.0以前は「ダイレクトメッセージ」と呼ばれていた公開範囲です。それが誰でも見れる状態になっていたわけです。

障害情報の公開後、早々に影響範囲と進捗について報告。

約2時間後に原因の特定に至りました。

そのあとも影響範囲の限定や誤解・混乱を防ぐための投稿をいくつかしていますが、ひとまずこの記事ではここまでです。

重大な問題なのに気づくのに2週間もかかった

さて、今回の障害の原因はMastodonの依存ライブラリのアップデートによる破壊的仕様変更によるものです。
破壊的仕様変更って簡単に言うと、お店に行って「りんごをください」って言ったらりんごが出てくるとします。でもある日から「りんごをください」と言ってるのにみかんが出てくるようになります。みかんをりんごだと言われて渡されるわけです。でも店員の頭の中ではみかんがりんごということになってるのでそれで大丈夫ですし、こっち側もみかんをりんごだと思って使わなくちゃいけない。同時に自分の持っているみかんに「これはりんごです」という注意書きをしなくちゃいけない。こういう自分側でも修正が必要な状態になります。

破壊的変更って事前にどこかで警告を出しているはずなのですが、自分はそれを把握していませんでした。Mastodonのデバッグに定番の「foreman start」を使っていましたが、使ったことのある人わかると思うんですけど、これはログがすごい勢いで流れます。ログが多すぎて読めません。警告も絶対この中に表示されていたはずなのですが、もちろん読めません。
ログが読めないのであれば事前の情報収集が必要ですが、これもネットで軽く調べた程度だとまず出てこない。
なんちゃってで改造していたら、この問題にはアップデートするまで気づきません。

じゃあ自分はどのタイミングで気付けるのかというのが問題ですが、この障害には1つ面倒な点がありました。だいいち、こんなめちゃくちゃ分かりやすい障害だったのに、発生した14日から29日まで、約2週間、あすか含めて誰も気づかなかったのです(気づいて報告しなかった人もしかしたら多そう)。

この障害について7月29日、利用者から報告が2件ありました。しかし最初の1件は報告が来た後すぐ消されて、そのあとpixivのダイレクトメッセージで来てました。当日の夜(一通り済ませた後)まで気づきませんでした。(今回の障害の性質を考えると仕方ないことですが、今後はMastodon内で伝えられないならメールで連絡するようお願いしました)
その2時間後に別の人から来た報告でようやく気づきました。その方もああいう障害の中であえてMastodon内のダイレクトメッセージ選ばれたので、相当勇気必要だったんじゃないかと思います。

で、最初の1件はお知らせアカウントに返信として来ました。新しい返信がありましたよと、Mastodonはメールで通知してくれます。もちろんその中に障害内容もしっかり書いてありました。
その時点で気付けよという話ですし、もちろんこれが事実であればかなり重大なことなのであすかも10分かけて調べたのですが再現できなかったので、報告した人も後で勘違いに気づいてすぐ消したのかなーなどと思ってしまいまして、いったん無視してます。

どうしてそこまで情報があったのに気づかなかったのかという話ですが、その報告の中には、障害の発生条件が書かれていませんでした。その発生条件というのが、『ログインしていないこと』でした。

そもそも今回の障害、『ログインしていない状態で』画面を開かないと確認できません。あすかは普段kmyblueをログインしたまま使っていますから、もちろん気づきません。今後障害について調べる時に考慮に含めるようにしたのはともかく、これでもう1つ別の問題が出てきたわけです。

Mastodon本家に追従していくうえでの今回の問題の本質

当時、自分はMastodonを改造する時に動作確認をどうしていたかというと、自分で画面見て、画面を実際に操作していました。連合関係の機能(絵文字リアクション)についてはテストサーバーを建てて確認もしていました。全て手作業です。

で、どこからどこまで動作確認していたかというと、変更した箇所のみです。例えば新しい公開範囲を追加した時にすべての公開範囲で動作確認はしますが、無関係の場所、例えばリストの動きや設定画面までは確認しません。

そして、本家に追従するときも、普通にマージできていればそれでよし、手動マージも慎重に確認をして必要を感じた時に動作確認もついでに行っていました。

でもこういうやり方だと上記の障害には気づかない。どうしてかというと、障害のきっかけはただ1つのライブラリをアップデートしただけです。ライブラリのバージョンが変わっただけで、Mastodonのコードで手動マージが起きたわけではない。自分のプログラムに最初から問題があったわけではなくて、実際、アップデート前であれば正常に動いていた。当たり前のように使っているメソッドの動作が変更されただけ。じゃあこれ動作確認できるかと言うと、ライブラリのバージョン以外何も変わっていないので、上記の基準だとどこも確認する必要はない。確認しようがない。これで通ってました。

しかも動作テストももちろんしてたのですが、ログインした状態で行っていました。ブラウザがログイン状態を保存してくれますので、それで非ログイン状態で画面を開くことはめったになかったと思います。

非ログイン状態って、自分が普段使っている状態ではない。正常なプログラムがある日突然バグったとしても、すぐ気付けるような条件ではない。だからこそ難しいんです。
これからさらに改造するのだったら、本家でライブラリのバージョンアップがあったりするたび、いちいち全機能全部をすべての条件で点検しなければいけないのかと。会社でやっているように大量の手動テストを毎回やらなくてはいけないのかと、それはもう趣味の範疇を越えていて個人では不可能ではないかと。そんなところで悩んでいました。

Mastodonと自動テスト

Mastodonにはrspecという自動テストがあります。自分?自慢ではありませんがもちろんメンテしてませんでした。なので、自分のプログラムを変更するたび、毎回エラーが出ている状態でした。

障害が起きてしばらくして、気分も落ち着いてきて、上記の問題を解決するにはどうすればいいかと考えて、その時に自動テストが出てきました。
自動テストやってみようかと思って久しぶりにGitHub開いてみたら、このようなプルリクエストがあがってまして、それが自分にとってはとてもタイムリーでした。

CIテストについて改めて説明しますと、gitでなんかコミットするたびに毎回毎回毎回毎回システム全体をテストしてくれるシステムです。画面テストとか(当時はElasticSearch連携テストもなかった)細かいところに手は届かないものの、かなり助かるものです。

なのでライブラリをアップデートしたら非公開投稿が垂れ流しになったという、『ライブラリアプデしたら何が起こるかわからない状態』もかなり良くなります。
実際、テストで慢性的に発生していたエラーを取り除いた上で障害発生時のコードでテストを走らせてみましたが、破壊的変更の影響を受けた問題の部分でテストが落ちました。ライブラリのアップデートよりも前のコードでもテスト走らせましたが問題ありませんでした。
つまり上記障害は、自動テストをちゃんと整備していれば起こらなかったものです。

自動テストも銀の弾丸ではないといえばそれはそうですが、その前提となる基本もできてない状態でして、端的に言えば今回の障害がきっかけでkmyblueはテストを整備するようになりました。

MastodonにおけるCIテストの整備状況

テストの話って、あまり目立つものではないです。地味。とても地味。プログラミングを知らない人にとって魅力的な話ではない。
なので今まであまり語ってきませんでしたが、この機会ですから、Mastodonやkmyblueにおいて自動テストが現在どういう使われ方をしているか書いてみます。

意外かもしれませんが、本家Mastodonってテスト網羅しているわけではないです。テストが書けるのに書かれていない機能も意外とあって、例えばフォローしているアカウントの新着投稿を通知してくれる機能。これ実際に通知する部分はテストないんです。

ここに「ハッシュタグをフォローする」ボタンありますね。ここでタグをフォローしたら、フォローされたタグの投稿がホームに流れてくるんですが、フォローを外したらそれらの投稿もホームから消えます。でも自分のフォロワーの投稿は残してくれます。便利ですね!ちなみに自分の投稿は消えます
この機能についてのテストもありませんでした。(このバグ修正とテスト追加が自分の本家への初コミットだったりする本当はこれより前にもう1つPRあるんだけどまだ通してくれない

あと、本家Mastodonにはサークル投稿という機能があって、本当はFedibirdのようなサークル投稿ができなくても受信だけならできるはずなのにバグで受信できない。そのサークル投稿の受信についてのテストコードもありません。
このほかにも、覚えていませんがいくつかの機能のテストが書かれていなくて、その中にはみなさんが普段使っている機能も含まれます。

あ、ひとつ思い出しました。MastodonってElasticSearchと連携して全文検索が可能です。でも、その検索に関して、検索で出てきてはいけない投稿(indexableよりも前の、昔からあったルール)っていうのがあります。それに関するテストが、本家でElasticSearchのテストが追加されたのはつい最近ですけども、その中にない。StatusesSearchServiceクラスを通してテストしてくれないと、本番の動作を保証するには不安すぎる。これも皆さんが普段使っている機能です。

といってもこれはMastodonに大量にある機能のうち、ごく少数の例を恣意的に持ち出したにすぎません。Mastodonの多くの機能はしっかりテストでカバーされています。細かいところで漏れがあって、その中に私達が普段使うような機能も含まれているというだけです。

それから、本家Mastodonではテストコードの書き方がばらばらです。describeやcontextをきちんと使うタイプ、itの中に全部入れちゃうタイプ、subjectに書くべき処理をbeforeから呼び出しちゃうタイプ、様々です。

kmyblueにおけるテスト整備

こういうテストでどうして本家が今まで回っていたかと言うと、単純に本家Mastodonは利用者が多く、バグ報告もすぐあがってくる。自分がやらなくても誰かがすぐ報告してくれる。それが成立するほど、利用者がとても多いんです。

でもkmyblueはそうではない。多くても2000いくかどうかです。今回のように重大な障害に2週間も気づかないってことがまた起きるのも、ありえない話ではないです。

なのでkmyblueのテストは、本家Mastodonよりも細かく書いています。特にプライバシーに関係する部分は、全パターンとまではいかなくても、プライバシーが気になった部分はなるべくテストケース作っています。
本家の機能でもプライバシーに関係した場所のテストは増やしています。例えばPublicFeedというクラス。これ、ローカル・連合タイムラインに表示する投稿を選ぶ処理が入ってます。そこで、本家のコードだと「公開」「フォロワーのみ」2種類の公開範囲しか確認していない。kmyblueでは「ローカル公開」という独自の公開範囲の他、「未収載」「指定された相手のみ」の公開範囲の確認もついでに足しています。

テストって通常、多数あるケースの中からサンプルを抜いて確認します。でもkmyblueって利用者が少ないから余計に心配なんですよね。1万通り総当たりは無理でも、懸念のあるところはなるべく埋めようと思ってます。

独自機能のテストもあります。例えば最初に挙げた絵文字リアクション機能。今はスタンプに名前変わっていますけども、そのテストも書いています。
それから参照・引用、絵文字リアクションを許可する範囲、投稿検索を許可する範囲、ドメインブロック、アンテナ(購読)、サークル、相互投稿などの主要な独自機能のテストも足していますし、管理人にしか設定できないNGワードといった、目立たないけど重要な機能についてもテストを作っています。
まだまだテスト作ってない独自機能や仕様もありますが、プライバシーに懸念のある機能はほとんど終わってますし、ともかくこれで十分ではないかと思います。

そのほか、本家の機能でkmyblueの改造が加わっていなくても、さっき挙げたように気づいたものは独自にテストコード足しています。英語苦手なので本家にPRしたくないけど。

なのでkmyblueのテストコードは本家が書くより長い。それでも、少しでも安全を確保したいと、そういうふうに思っています。

特に今はkmyblueって、フォークとして整備してるんです。誰でもkmyblueを使って自由にサーバー建ててもいいですよと、実際にそれで建ってるサーバーがいくつかあります。それらのサーバーの動作も保証しなければいけない。動作確認には責任がつきものだと感じてます。

テスト書くのって大変なの?

逆です。開発が楽になってます。

そもそもMastodonをデバッグする時にforemanを使うんですけど、Ubuntu自体が仮想マシンなんですよね。仮想マシン+foremanで、遅い。動作ももっさり。
それだけでなく、特定の機能を使うために自分で条件を整えなくちゃいけない。ある投稿を引用する動作確認であれば、まず引用される投稿を作らなくちゃいけない。公開範囲の絡む仕様であれば、それぞれの公開範囲の投稿を作らなくちゃいけない。前提条件として、先方のユーザー設定も変えなくちゃいけない。場合によっては、データベースから情報を直接変更しなければいけない。

それがテストで簡単になってます。テストは前提条件の整備を自動化してくれます。書くのが大変といえば大変ですが、動作のもっさりしてるforemanを操作して複雑な状況を作るより早いのは明らかです。

複雑な状況を求めるほど、人的ミスは必ず発生するものです。それもありません。
ただ、テストコード自体にミスがあるかもしれないので、結局は手動での確認が必要になる場合もありますけどね。

また、連合機能のテストにおいても効果を発揮します。サーバー同士でActivityをやり取りする時、特定のActivityを受け取った時にどういう動作をするかの確認。特に攻撃目的で作られた不正なActivityへの対応。これも、テストでスタブを作ることで、わざわざテストサーバーを2つたてなくても簡単に確認できます。
もっともテストサーバー2つたてないとわからないこともあります。スタブを設定するのは簡単ですが、スタブの代わりに「実際に出力されたActivity」を使って入力処理をテストするのは難しいからです。とはいえ、ある程度テストでバグを潰した上でテストサーバーを建てて確認するメリットはやはり大きいです。テスト環境でのデバッグ作業がかなり省力化されます。どうでもいいバグはあらかじめ落としてありますから、動くところはわりとすんなり動きます。サーバーの動作がおかしい時に、わざわざエディタを開く。これを何百回も繰り返す手間がほとんど発生しません。

テストの基本的なメリットもちゃんとあります。毎回同じ条件で同じ項目を、ヒューマンエラーの介在することなく、きちんと確認してくれます。そして人が作業するより高速です。これらはMastodon本家のライブラリのアップデート(Rails 7.0から7.1とか)のときにかなり助けになります。

このことから、テストはデバッグにすら使えると思ってます。自分は特に大きい機能を作る時、先にテストを書いてからプログラムを修正することも多いです。

まとめ

で、こうしてテストを増やしすぎた結果、kmyblueはおそらくMastodonフォークの中でもかなりテスト整備されたほうになったのではないかと思ってます。そして本家の機能にも手を加えてるどころか、本家のテストコードの間違いを修正したりもちょっとあるので、実は理論上では本家由来の機能であっても本家よりちょっとだけテストが充実してたりします。

テストは確かにkmyblueの最低限の品質を保証しますが、万能ではありません。例えばkmyblueは画面のテストを作っていません。なのでWebクライアント側でバグがあっても、テストでは気づきません。そこは今まで通り手作業です。
バグによる情報漏洩のリスクを踏まえるなら、テストはバックエンドだけで十分でクライアントは手作業でもいいんじゃないかという認識でいます。直接SQL呼び出したりしない限り、フロントエンドだけで情報漏洩が起こるリスクは皆無です。とはいえそこもテストがあったほうが安全といえば安全。めちゃ時間かかるけど。

でも、バックエンドでもテストでカバーし忘れたところがまだ残ってて、そこから情報漏洩が起こるというリスクはまだあります。どんなに気をつけていても、テストのカバー範囲から漏れるというのは起きます。kmyblueフォークを使ってる人もいるのですから、大きな事故にならないようこれからも引き続き気をつけていかなければいけないです。

この記事が気に入ったらサポートをしてみませんか?