マチマチ技術ブログ

ご近所SNSマチマチの開発チームよりお届けします

続・マチマチとOSSの関わりをGitHubで振り返る

ご近所SNS「マチマチ」を作っている武者(@knu)です。前回に引き続き、今回もマチマチとOSSとの関わりについて紹介したいと思います。

不具合のフィードバック

それなりの規模のプロダクトを開発していれば、当然さまざまなライブラリやツールを使うことになりますが、自分達では特に変わった使い方をしているつもりはないのに、ふとバグに当たることがたびたびあります。認知バイアスの可能性もありますが、しょっちゅう「なぜ我々が第一発見者なのか?みんな踏んでいないの?」と感じている気がします。

そこで思い至るのは、「バグをバグであると認知して指摘する」こと自体が、スキルや経験の裏打ちを必要とする所業なのかもしれないということです。

自分に自信がない、ちゃんと調べる気に至らない、などの引っ込み思案に陥り、「きっと自分の使い方が悪いのだろう」「回避したら動いたからよしとしよう」と撤退してしまうエンジニアが思った以上にたくさんいます。明らかに想定外の使い方をしていながらバグだと思い込んで騒ぎ立てるような人もいるにはいますが、もっと恐れずにフィードバックをしていいのにと思います。

「こういう使い方ができると思った」「そこでこう書いた」「しかし結果は期待と違ってこうだった」という筋道立った報告は、それがバグでなく仕様だったとしても、開発者にとって貴重でありがたいフィードバックになっていることも多いです。直接的にはドキュメントの充実、中長期的には勘違いの起こりにくいAPIを設計するための参考になります。

というわけで張り切ってバグ報告していきましょう!

PDFのサムネール取得

マチマチでは、自治体の配布するチラシや広報のサムネイル画像を作るためにgrimというgemを使っています。特定のファイルをアップロードするとエラーになる事象があり、調べたところ、ファイル名に半角括弧「(」が含まれるとエラーが出ると分かりました。恐る恐るgemのソースを確認すると、見事なエスケープ漏れでした。(つまり脆弱性…😱)

Save file to path which includes shell meta character by fujimura · Pull Request #35 · jonmagic/grim · GitHub

shellescape は私が実装したメソッドだけに、始めからちゃんと使ってほしかった!と思いました。最小のパッチで投げてありますが、本当は引数列を shelljoin で結合する形にしたいところです。

PRを出したときは、マージされるまでは一時的にgithub sourceでforkを使い、マージ後に新しいバージョンが出たらsourceを戻してbundle update {gem名}する、という手順になります。

日本の電話番号

マチマチでは、ユーザのSMS認証やローカルプレイスデータの整備で数多くの電話番号を取り扱います。電話番号のバリデーションや整形表示(適切な桁数で区切りを入れる)など便利な機能を提供してくれるのが、phonyというgemです。

このライブラリが0800で始まるフリーダイヤルの番号を正しく扱えなかったため、調べたところ、データに誤りがあると分かりました。0800の後には6桁ではなく、7桁の番号が続きます。(0120より1桁多い)

Japanese freephone starts with 0800 has 7 digit in local part by fujimura · Pull Request #424 · floere/phony · GitHub

PRを出し、正規の出典(現総務省サイトにある当時の郵政省のプレスリリースのURL)も示して解説を添えました。マージの判断に必要な情報を提供するのも、素早くマージしてもらうためには大事なことです。

ActiveSupportのおせっかいに潜むバグ

RubyにはBourne shell, Perlの伝統を引き継いだシェルコマンド展開 `…` が実装され、 Kernel#` メソッドを通じて提供されています。

このメソッドは、コマンドが存在しないなどの場合には Errno::ENOENT 例外が発生しますが、それ以外はコマンド自体がエラー終了した場合でも必ず出力文字列を返すはずです。ところが、 rails runner を通じて実行したスクリプトの内部の `…`.chomp の部分で、なぜか NoMethodError (undefined method`chomp' for nil:NilClass) で落ちているのが発見されました。

ありえない…と、調査したところ、なんとActiveSupportが Kernel#` をオーバーライドしており、その中の処理に問題があることが分かりました。

Make the backquote operator always return a string by knu · Pull Request #31253 · rails/rails · GitHub

コマンドが存在しないときに Errno::ENOENT を握り潰して、標準エラー出力にその旨を表示しています。悪いことに、 rescue 節が STDERR.puts の呼び出しのまま終わっているため、 IO#puts の返り値であるnilが返却されてしまうというわけです。1

背景としては、Unixではメタ文字を含む場合はシェルを介し、含まない場合直接 execve(2) するようになっており、コマンドが存在しない場合の挙動がシェルを介するかどうかで変わってしまうことから、与えた文字列やプラットフォームの事情に依らず同じ動きに揃えたかったということのようです。

私の最初の提案は、握り潰すのであれば空文字列("")を返すようにしよう、というものでしたが、最終的には歴史的経緯はともあれRuby本来の挙動のままでいいだろう、と削除されることになりました。

これは合意が取れてからもかなりマージに時間が掛かりました…。

`…`の未来

なお、これとは別にMatzから出た話として、 `…` は将来的に廃止して ` を別の意味に使いたい、というアイデアがあります。標準出力はキャプチャするがデフォルトでエラー出力をstderrに出してしまう Kernel#` の挙動がいまいちである、などの理由付けもありますが、Rubyの文法を拡張したい何かが念頭にあるようです。(「ネームスペース」というワードは出ていますが実体はまだ謎に包まれています)

昨今はRubyの用法の多くがWebプログラミングの文脈となり、昔ほどシェルコマンド展開を使わなくなったということもあるでしょうか。そもそもオリジナルのBourne shellでは $(…) という代替記法の方がどちらかといえばメジャーですね。

また、Go, Haskell, ECMAScriptなどでそれぞれバッククオートが有効活用されている様子は、ASCII記号の枯渇と戦っているRubyからは魅力的に思えます。

どうなるかは分かりませんが、現在の `…` が廃止された場合も %x[…] 記法は残されると思うので、僅かな変更(従来の `…` の頭に %x を付ける)で従来のコードを動かすことはできそうです。(自動修正も可能でしょう)

ActiveRecordのバグ

ActiveRecordには、巨大な結果セットを OFFSET を進めながら LIMIT で少量ずつ取ってくれるfind_each / find_in_batches という便利なメソッド(ActiveRecord::Batchesモジュールが提供)がありますが、これはプライマリーキーの昇順でしか行えないので、時には手で OFFSET / LIMIT を指定してループしたいことがあります。

relation = Places
  # …
  .joins(city: :prefecture)
  .select("places.*")
  .select("cities.code")
  .order(Arel.sql("cities.code ASC"))
  .order(:id)
  .distinct
    
batch_size = 100
    
0.step(by: batch_size) do |offset|
  records = relation.offset(offset).limit(batch_size)
  break if places.empty?
    
  # placesを使う
end

こんなコードを書いていたのですが、なぜか、 break if places.empty? で抜けず下に進んだのに、 places を使おうとすると中身は空、という事象が発生していました。

調べてみたところ、ActiveRecord::Relationが empty? / exists? でレコードの存在を安価なコストでチェックするために SELECT 1 AS one … LIMIT 1 というSQLを発行しており、これが DISTINCTOFFSET を併用した際には実体と食い違う結果を招いていることが原因でした。

このSQLには DISTINCT が付いていないため、重複レコードによる水増しでやっと OFFSET を超えるようなケースでは、 DISTINCT の下では取れないのに「取れる」と誤判定してしまうわけです。

Relation#exists? and empty? return the wrong result when used with distinct() + offset() · Issue #35191 · rails/rails · GitHub

ここの修正は職人芸が要るな、と思ったので、PRでなく再現ケースを書いて単にissueを上げたところ、すぐにkamipoさんが現れて直してくれました。

Railsの新しいリリースまでには間があるので、 .to_a を付けてRelationを実体化(配列化)してしまうことでworkaroundとしています。

Cloudflareのキャッシュパージ

マチマチではCDNとして主にCloudflareを使っています。メディアサイトの本格始動以降、コンテンツの拡充とともにUUもPVもまさに桁違いに激増し、今も急成長しているため、CDNのエッジキャッシュで多大な恩恵を受けています。

しかし、キャッシュが難しいのはみなさんご承知の通りです。データの変化と連動して適宜関連ページのキャッシュをパージすることで、コンテンツの更新がいち早くユーザに届くようにする必要があります。キャッシュのパージにはAPIを用いるのが通例です。

CloudflareのAPIはシンプルなので、ふつうのHTTPクライアントライブラリを使っても十分ですが、せっかく専用ライブラリがあるのなら、使いやすさや保守性を求めて乗っかりたいところです。

というわけで、いくつか比較検討した結果、まずはcloudflairというgemを使い始めました。なお、最初はキャッシュパージではなくDNSレコードの操作のために使っていました。ところが、キャッシュパージAPIを同ライブラリから呼び出そうとしたところ、エラーが返ってきます…。見てみると、POSTを発行すべきところ、DELETEを発行していました。キャッシュを消す、というイメージに引っぱられてしまったのでしょうか…。

purge_cache is broken · Issue #10 · ninech/cloudflair · GitHub

よく見ると採用後にコミットがないし、アクティブではなさそう。キャッシュパージをするユーザがいなかったので発覚せず、結局そのまま静かに眠りに就いてしまうのでしょうか…。

やむを得ず、前回比較した際の対抗馬だったcloudflare gemへの移行を進めました。独特の非同期フレームワーク(async)をバリバリに採用しており、そこまで性能を重視しない身からすると余計な面倒は避けたい、という心情から前回の選定では見送ったのですが、こちらの方が活発に開発が進行しており、賭けに負けた敗北感を味わうことになりました。

無事、DNS部分の移行はすんなり行ったので、「ああよかった、ではキャッシュパージを…」と進んだところで、再び愕然とすることになります。なんと、このgemもキャッシュパージAPIの実装に誤りがあったのです。コードの意図としてはパス名に指定しているつもりの purge_cache が、キーワード引数のミスマッチにより、あろうことかURLパラメータ ?path=purge_cache に化けてしまっている…。 さっそく修正、テストしてPRを出しました。

Fix purge_cache by specifying a path instead of a `path` parameter by knu · Pull Request #46 · socketry/cloudflare · GitHub

こちらは執筆時点で取り込み待ちですが、いずれマージされると思います。

結局、「誰もキャッシュパージにgemを使っていなかった」が結論ということでよろしいかと思います。なんてこった…。😩

機能拡張のフィードバック

差分データの展開

2つのデータの並びを比較して、差分を計算するdiff-lcsというgemがあります。2つのファイル(バージョン)の内容の行単位の差分を取る diff(1) で使われているMcIlroy-Hunt longest common subsequenceアルゴリズムをライブラリ化したものです。

マチマチでは、一部コンテンツの差分更新を行う処理でこれを用いています。開発時、まずREPL (irb/pry)でDiff::LCSの挙動を軽く確認した上で、実際のコードに落としていきました。REPLでの表示の様子から、てっきり差分リストの各要素は配列だと思い、以下のようなコードでブロック引数への多重代入で受けようとしたのですがなぜかうまく動きません。

Diff::LCS.sdiff(a, b).each do |action, (old_position, old_element), (new_position, new_element)|
  case action
  when '!'
    # replace
  when '-'
    # delete
  when '+'
    # insert
  end
end

diff-lcsの実装を見たら、このオブジェクトは配列ではなく、配列っぽい文字列表現を採用していただけでした。REPLでの見た目にだまされた…。

オブジェクトを単値で受けて .to_a してやれば分解できるのですが、それはあまりに面倒だと思ったので、脊髄反射的に to_ary を実装するだけのPRを出しました。

Add #to_ary to Diff::LCS::Change and Diff::LCS::ContextChange by knu · Pull Request #47 · halostatue/diff-lcs · GitHub

CIがこけたら直そう、と気軽に出したのですが、あっさり通ってしまい、ほどなくマージされます…。

ところが、改めて手元でテストを動かしてみると大量にこける…。中を見ると、差分リストを flatten しているところがあり、 to_ary があるために、今まで保存されていた各要素の中身もすべてflattenされてしまったというわけです。

作者の方もこれに気づき、revertした上で改めてflattenしていたところを書き直してくれたので一件落着です。なお、CIは設定が壊れていてテストが走らずに通ってしまっていたようです。

メソッドが一つ増えただけなのに壊れてしまう、というやや珍しい例で、次回からは気を付けようと思います。😅

Bulk InsertをPostGISでも動かす

マチマチでは、各地域情報の充実のため、大規模なデータ投入を定常的に行っています。まとまったデータを投入する際、1レコードずつINSERTしていては性能が出ないので、個人的に利用経験のあったbulk_insert gemを使おうと思いました。

ちなみにactiverecord-importというgemもあり、実際これを使ったこともあります。こちらはActiveRecord親和的なAPIで、オプショナルですがvalidationを行わせることもできます。ただ、モデルのデータ構造を介してしまう分オーバーヘッドがあります。

bulk_insertの方は、生のINSERT文を発行することに特化しており軽量です。自分でループを回してbulk_insertに食わせていくと、適当な数(デフォルトでは500)ごとに自動でINSERTを発行してくれるのが便利です。今回は、データの正しさはあらかじめ確認できているためこちらを使うことにしました。

そういえば、Rails 6にbulk insert機能が入るかどうか、ちょっと注目ですね。☝️

さて、このbulk_insertを使おうとしたところ、 ignore: true モード(重複は無視=スキップ)がactiverecord-postgis-adapterに対応していないことが分かりました。

見てみると、PostGISアダプターだけでなく、少なくとも日本ではいちばんユーザが多そうな Mysql2 アダプターや、私が前職でも使っていたactiverecord-mysql2spatial-adapterにも対応していないようだったので、ついでに対応させるPRを出しました。

Support more adapters including Mysql2, Mysql2Spatial and PostGIS by knu · Pull Request #27 · jamis/bulk_insert · GitHub

昨日の自分は明日の誰か、今日の誰かは明日の自分。みんなつながっています。😇🤝😇

開発便利ツールの公開

開発に使う道具も、みんなで共有するメリットはたくさんあります。いいものを作って気に入っていても、それにばかり手をかけるわけにはいかないことも多いです。となれば、公開してユーザが集めるのがリスクと手間を減らすことにつながります。周辺の状況が変化して有用性が失われそうになったときに、自分の代わりに修繕・改善してくれる人が現れてくれたらうれしいですね。

RSpecのマッチャー

WebのE2Eテストでは、UIを中心に非同期処理が多いため、そうした面でRSpecのマッチャーの支援を期待したいところです。

幸い、Capybaraの have_content とか have_current_path などのマッチャーは所定のタイムアウトまでに条件が成立するかをポーリングしてチェックしてくれます。では、サーバサイドのデータの状態が非同期に更新される場合の検査についてはどうか。

…残念ながら見当たらなかったので、所定の時間内に条件が成立するかを見る become マッチャーを作って使っています。

GitHub - fujimura/rspec-become-matcher: RSpec matcher to check that an expression changed its result in arbitrary seconds

マチマチにおけるテストの方針についてはこちらの記事もどうぞ。

tech.machimachi.com

db/structure.sql の自動マージ

マチマチでは、各種拡張(extensions)、制約(constraints)、トリガー(triggers)、 ON DELETE 節等々、PostgreSQL/PostGISのさまざまな機能を使い尽くしているので、スキーマを db/schema.rb で表現することはとうにあきらめています。(config.active_record.schema_format = :sql)

そこで問題になるのが、「スキーマ変更が交錯すると毎回必ずconflictが起きる」ことです。というのも、 db/structure.sql はほぼ生のSQLダンプファイルで、末尾はこのように schema_migrations テーブルへのINSERT文になっています。

INSERT INTO "schema_migrations" (version) VALUES
-- …
('20190219072917'),
('20190219084823'),
('20190220030558');

これでお察しの通り、途中まで , で最後だけ ; が付くため、この後に何か追加されると

-('20190220030558');
+('20190220030558'),
+('20190221012345');

のように行の追加だけでなく変更が発生してしまい、必然的に複数の差分が重なると自動マージが働きません。(仮に記号の問題がなくても、変更箇所が重なると自動マージは無理ですね…)

これは色々な機能を並行開発している際にかなり辛いので、 db/structure.sql 専用のGitのマージドライバーを書きました。

github.com

例によって、ついでにMySQLやSQLite3にも対応させてあります。

ドキュメントの通り、インストールは簡単にしてあるので、同じ境遇の方はぜひご利用ください。

Ridgepoleだとスキーマバージョンというものはないためこのマージ問題はなさそうですが、マージ問題だけで db/structure.sql をやめてRidgepoleを使えないか検討している、といった向きにはこのツールは朗報かもしれません。

このマージドライバーは、構造上差分が生じてしまう schema_migrations などの部分だけ自力マージして、後はGitのデフォルトマージドライバーである git merge-file に任せる実装になっています。

スクリーンビデオキャプチャのGIF化

表示/UI系のissueやPRを出すときは、スクリーンショットを付けないと何のこっちゃとなりがちです。ところが、GitHubには動画をインラインで貼ることができません。唯一貼れるアニメーションGIFも、ファイルフォーマットの制約で256色しか使えません。

<FONT COLOR="#ffcc00">…</FONT> (Webセーフカラー216色を意識)で表示もビットマップフォント、なんて時代から過ごしてきた身からすると、「よほどきれいな写真でも入っていない限り、256色もあれば行けるんじゃね?」とつい思ってしまうのですが、流麗なフォントスムージング、丸みを得た枠線、アルファチャンネルによる半透過効果などなど、今やごくシンプルに見えるページでさえレンダリングには数十のオーダーで色が使われるようになっています。パレットの最適化を行わずに素朴にffmpegなどで変換すると、ディザリングの嵐となってまだら模様が目立ってしまい、とても見られた動画にはなりません。

何かいいツールはないか、と思ったら、こんな記事を見つけました。

cassidy.codes

ffmpegのパレット生成機能を使い、2パスで変換すると、きれいにエンコードできるというお役立ち情報です。これは良いことを知りました。

ということで、シェルスクリプトを書くだけでも良かったのですが、MacのUIから楽に呼び出せるようにAppleScriptでくるんでアプリにしてみました。

github.com

ところで、AppleScriptってどうやって体系的に学ぶんでしょうね。私は毎回ググっては切り貼りして、試行錯誤で七転八倒しています。

Markdownでチェックボックス付きリストを使う

esaやGitHubのissue/PRでチェックボックス付きリストを書くことが多いので、Emacsの markdown-mode にチェックボックスを入れる機能を追加しました。

ブラウザ上のテキストエリアも、GhostText(Chrome, Firefox)を使えば好きな外部エディタで編集できます。

Add `markdown-add-gfm-checkbox` and make it a final fallback for `markdown-do` by knu · Pull Request #229 · jrblevin/markdown-mode · GitHub

巨大YAMLの編集で迷子にならない

Webの開発をしていると、なぜか巨大なYAMLファイルと格闘するはめになることが多いですね。

RailsでもメッセージカタログはYAML形式だし、CIの設定ファイルもYAMLで、最初はシンプルでもいつのまにかビルド手順やマトリックスが複雑になって肥大化していきます。JSONと違ってコメントも書けるけれど、そもそも今自分が見ている行はどこなんだろう…という問いから逃れるのは至難です。

私はEmacsを使っているので、 which-function-mode および imenu という仕組みを使って今いる行のYAMLパスを常時表示できるパッケージを書きました。階層を選んでジャンプもできます。

github.com

おわりに

今回は少し長くなってしまいました。事の経緯をトラブル発生から描くと、一つ一つが物語になってしまいますね。

日々直接携わっているプロダクト・サービス自体以外にも、この事業をしていなければこの世にこのタイミングで起きなかったかもしれないことがいろいろあるなあ、と振り返ることができました。

自分達が困ったから直す、あるいは作る、が基本ではありますが、きっと誰かの役にも立つはず、という思いで一つ一つの問題に取り組むことは心を豊かにしてくれます。

引き続きマチマチでは、OSSで最高にハイになりたいエンジニアを募集しています。武者にお気軽にお声掛けください!

Wantedlyもあります!

www.wantedly.com


  1. 冒頭の話と絡めると、返り値がnilだと、「コマンドが存在しないときはそういうこともあるか」と妙に納得してしまい、自分のコードを修正してしまうユーザもそれなりに多そうです。

マチマチとOSSの関わりをGitHubで振り返る

ご近所SNS「マチマチ」を作っている武者(@knu)です。今回はマチマチとOSSとの関わりについて紹介したいと思います。

マチマチのエンジニア達はもっぱらRuby/RailsやReactのコードを書いていますが、そうしたフレームワークや周辺ライブラリ、そして言語実装はすべてOSSとなっていますし、データベースエンジン(PostgreSQL/PostGIS)、RedisやMemcached、それらを稼働するOS、ビルドやテストに使うツール、エディタ、等々に至るまでの多くがOSS、またはOSSをベースとしたものとなっています。今や、OSSなくしてはこの世界で何もなし得ないと言っても過言ではありません。

私自身も長らくOSS開発に携わっており、前世紀からFreeBSDRubyのコミッターを務め、自作のOSSもたくさん書き、周辺のエコシステムにパッチをフィードバックしたりしてきました。そんな中で強く感じるのは、近年ますますOSSと仕事のコードに境目がなくなってきているということです。

受託であろうが自社製品の開発であろうが、OSSを使って何かを書いている限りは、日々不具合や改善したいと思う点が必ず出てきます。ソフトウェアの開発の動きが活発な現代では、改善を自分達で独占すべく手元で抱え込もうとしても、長期的にはメンテナンスコストで持ち出しになる、よほどのコアコンピタンスでもなければ公開して開発元にフィードバックして後を任せた方が楽である、という考え方はごく普通になっています。

マチマチでも、社内での成果を社外に出すことについていちいち許可を求めるような手続きはなく、逆に「貢献だ!恩返しだ!」と肩肘張ることもなく、淡々と、よりよい世界を目指す一住人としてOSSを公開したり開発元にフィードバックしたりしてきました。

私がジョインしてからまだ2年足らずですが、主にGitHub上の活動から改めて振り返って、我々の足跡をまとめてみました。

ドキュメント関係

BigQueryのDash用docset

github.com

BigQueryのSQLを書くときに、構文や関数についていちいちググるのがつらいことから、ドキュメントブラウザーDash用のオフラインドキュメントを作ってメンテナンスしています。SQL文は一般的な英単語の組合せですし、世の中には無数のSQLエンジンがあるので、検索ワードを調整するコストが高いのです。

これは私が前職時代に作ったものですが、現職ではますますBigQueryを多用するようになったので、引き続き更新を続けています。(設定画面の「Downloads」→「User Contrubuted」からインストールできます)

Dashのドキュメントはdocsetという形式で、ダウンロードしてきたオンラインリファレンスに、命令や関数を抜き出して作ったインデックス(SQLiteデータベース)を付加して同梱することで作成します。生成スクリプトはRubyで書いています。コードは豪快にRakefile一つで完結しており、正常にビルドできたかのチェックや前バージョンとの差分出力など、メンテナ目線で必要な機能を備えています。

GitHub - knu/docset-bigquery: BigQuery Standard SQL Docset builder for Dash.app

ちなみに私はPrestoのdocsetも同様にメンテナンスしていますが、前職ではAmazon Athenaで必要だったものの、現職では出番がありません。AthenaやTreasure DataなどでPrestoを使っている方は、活用していただければと思います。

GitHub - knu/docset-presto: Presto Docset builder for Dash.app

これらの定期更新のためのPR作成手順は自動化しており、それぞれ、オンラインドキュメントの更新を検知するとビルドが走り、自分のレポジトリにcommitしてpushし、ビルドログとともにプッシュ通知しつつ、TodoistにToDoとしても登録するワークフローを組んでいます。ToDo項目からはリンクを辿ってすぐにPRを出せるようになっています。

これには私が主要開発者の一人を務めるワークフローエンジンHuginnを使っているのですが、それの話はまた改めて紹介します。古くはYahoo! Pipes, 最近だとIFTTTやZapierのようなもの、というと通じるでしょうか。

github.com

いささかがんばりすぎ感はありますが、前回ビルドからの差分をいち早く目にすることで新しい機能や変更に気づける(Be the first to know)のは、一ユーザとしても大きなメリットだと思っています。

コードの健全性

マチマチのコードベースは、PrettierRubocopでソースコードの大部分を自動フォーマットしており、余計なスタイルのブレを排除しています。

これの良いところは、スタイルについての議論やレビューで余計な時間・ストレス・軋轢を生まない、というのがひとつ。瑣末なスタイルについてだらだら(自動で)指摘するなら(自動で)修正までやれよ、というのは誰しも思うことでしょう。

また、癖の強い書き手がいたとしてもその「におい」を消すことができるので、「ここは○○さん独特のスタイルのコードだからいじるのが憚られる」のような不健全な縄張り意識と属人化を防ぐメリットもあります。

「誰がどこをいじってもいい」を当たり前にすることで、プロダクト・チームは強くなります。

Rubocop

Rubocopには好みが分かれるcopが多くあり、かつ余計なお世話と思いたくなるものも日々追加されていきますが、マチマチでは自動修正をopt-inしていくだけなのであまり気にしていません。ただ、自動修正が壊れているのに気づいたときはすぐにフィードバックしています。

Rubocopのデフォルトは癖が強すぎるので、マチマチでもかなりカスタマイズした上で使っています。周りを見回しても、onkcopのような有名な設定例をベースに各現場でカスタマイズしていたりするところが多いようですね。

StandardRB

そもそも細かい設定ができすぎるのが混乱の元である、と喝破したのがPrettier(rubyプラグインも開発中!)やrufoですが、少し前に、皆が使い慣れたRubocopをベースにしたフォーマッタStandardRBが登場し、1.0に向けてフィードバックを呼びかけていました。

github.com

そこで、私も軽く試していくつかフィードバックしておきました。

最終的にどのフォーマッタが天下を取るのかはわかりませんが、もしStandardRBが本当に「スタンダード」になったらここはつらい、と思ったところはみなさんもフィードバックしておくといいと思います。

SQLの自動フォーマット

マチマチでは、プロダクトの内外を問わず、PostgreSQLやBigQueryで大きなSQLをたくさん書いて使っています。先に触れた通り、Ruby, JavaScript, CSS, JSXなどのソースファイルはすでに自動フォーマットを行っているのですが、SQLについてはまだ適用できていません。

すべて完璧にとは言わずとも、8割方を読みやすく整形してくれるものが見つかればいいのですが、マチマチではPostgreSQLやBigQueryのSQLをそれぞれ使い倒しているので、なかなか満足の行くフォーマッタが見つからないのです。SQLは他のプログラミング言語にはあまり類を見ない自然言語寄りの平坦な構造をしており、方言や拡張も無数にあるため、それらを広くカバーする実用的なフォーマッタの作成は困難なのでしょう。

個人的にはPythonのsqlparse(コマンド名はsqlformat)を試していますが、グローバルに適用するにはちょっと足りない感じです。たとえばSELECTの式リストを途中でいい感じに区切ってくれない、複文を食わせるとおかしい、などの問題を確認していて、直すためにはどういうルールを設ければいいのかもにわかには思いつかないことが多いです。今のところは、このツールでフォーマットしたものを手直しする運用で今は我慢しています。

私はEmacsを使っているので、簡単に使えるようにformat-allパッケージにSQL設定を追加してもらいました。

Define a formatter for SQL (sqlformat) by knu · Pull Request #11 · lassik/emacs-format-all-the-code · GitHub

名前の一括置換

Rubyコミュニティにおいては、まつもとさん(Matz)の座右の銘「名前重要」が広まっていますが、マチマチも開発においてモデルや機能の命名にはこだわっています。

また、開発を進める中で、ある機能が始めに命名されたときとは役割が異なってくることがあります。そういうとき、マチマチでは躊躇せずに実体に即した名前に変更してしまうことが多いです。

弊社藤村(id:fujimuradaisuke)の作ったgit-gsubコマンドは、そういうときにファイルの中に現れる名前やファイル名自体を一括置換するのに便利なツールです。

github.com

私の方は、Emacsの中で置換する際に便利なパッケージを作りました。

github.com

Railsでよくある命名規則に則って FeedEntry / feed_entriesFeedItem / feed_items に置換したい、みたいなことが簡単になります。名前の変更だけでなく、あるモデルのCRUDを既存の他のモデルのそれからコピペで作る、というのも管理画面などでありがちなので重宝しています。

おわりに

一記事に全部含めるには数が多かったので、本記事は一旦ここで区切りとします。次回をお楽しみに!

マチマチでは、空気のようにOSSを吸ってOSSを吐くエンジニアを募集しています。武者にお気軽にお声掛けください!

Wantedlyもあります!

www.wantedly.com

マチマチをGAEで動かす話

ご近所SNS「マチマチ」を作っている武者(@knu)です。夏生まれだけど暑い夏は大の苦手です。

マチマチとHeroku

マチマチでは、きめ細かい地域の情報を扱うそのサービス特性から、PostGIS(PostgreSQLの地理空間情報拡張)をかなりヘヴィーに使い倒しています。また、サーバサイドアプリケーションの実装にはRuby on Railsを採用しています。

これらを載せて動かすプラットフォームとして、マチマチでは当初Herokuを利用していました。Herokuはご存じの通りRails専用PaaSとしてスタートし、DBMSとしてPostgreSQLのみをネイティブ提供してきました。この組合せのPaaSとしては第一人者であり、今なお最適解の一つと言えます。様々なSaaSをプラグインの形で簡単に追加でき、デプロイも手軽かつ高速、といったあたりは、スタートアップにとって非常にありがたいサービスです。

ただ、マチマチなどの主に日本国内を対象にしたサービス提供者にとって大きな問題が、(「プライベートスペース」を除き)提供地域がアメリカとヨーロッパに限られることです。つまり日本国内にデータセンターがなく、すべての通信が太平洋を往復するため、大きなレイテンシーが生じてしまうのです。メインとなるログイン後のフィード画面の表示が完了するのに2秒近く掛かるようになった段階で、アプリケーションの最適化も必要だがインフラの移行も避けられないという認識になりました。

乗り換え先としてまずはAWSを考えましたが、スパイク対策に苦労が多い経験から、メディア露出が増えていくフェーズでの採用はためらわれました。マチマチはまだまだ小さなチームで開発・運用を行っているため、あまりインフラに手間をかけたくないのです。

GCPの誘惑

折しも昨年(2017年)はそんな我々にとって、Google Cloud Platform (GCP)が大きく存在感を示した年でした。3月、RubyをサポートするGAE Flexible Environmentが一般提供(GA)リリースされた上、Cloud SQL for PostgreSQLベータ版がリリース(その後2018年4月にGA)されました。突如、マチマチを稼働できるPaaS環境がGCP東京リージョンに誕生したことになります。Googleのインフラのスパイク対応の実績は折り紙付きですし、何より新しいものは試してみたい!

というわけで、機能開発が一段落した同年9月下旬から本格的に調査を開始し、さまざまな苦難を乗り越えて、最終的に11月下旬に移行を完了しました。本番稼働環境だけでなく、プレビュー・ステージング環境や開発プロセスの移行も行ったため、足かけ2ヶ月も要してしまいましたが、そのプロセスは、Herokuで完璧に構築された仕組みを自前の劣化コピーで置き換えていくような趣きがあり、ただただHerokuの偉大さが骨身に沁みる道のりでした。

Gitレポジトリへのpushをトリガーにして自動リリース、というのはどこでもやっていることですが、Herokuのそれはものすごく速くかつ安定しています。データベースのマイグレーションを挟んでのアプリケーションサーバの再起動・切替えは全自動ですし、ロールバックや他のバージョンへの切り替えも一瞬、環境変数の変更もheroku configで瞬時に反映できます。新しいブランチのプレビュー用アプリもコマンド一発で好きなホスト名でリリースできるし、データベースのコピーも簡単で、デモサイトのデータベースを定期的にロールバックするなんてのも高速に行えるので、多くのバージョンを効率よく同時運用する用途には持ってこいです。マチマチでは、メインサービスをGAEに移行した今でも、サブではHerokuを愛用しています。

デプロイという難問

他のPaaS同様、Google App Engine (GAE)にも良さと難しさが色々同居していますが、本記事ではデプロイについて軽く紹介します。

いきなり残念なお知らせなのですが、GAE/FEのデプロイは絶望的に遅いです。最小のRailsアプリでも、デプロイ完了までに7-8分は掛かります。Googleの強力な全自動インフラに載せるコストと思えばある程度しょうがないかと思いますが、それでもなお、マチマチの規模のアプリだと15分以上掛かっており、些細な変更のリリースでさえもCIと合わせると30分近く掛かってしまうのは辛いものがあります。

デプロイ方法自体は簡単で、Dockerfileapp.yamlを用意してgcloud app deployを実行するだけです。しかし、DockerイメージのベースとするRuby用の公式イメージに入っているRubyやNode.jsは常に少し古く、Yarnは入っていません(執筆時点)。そこで、rbenvで新しいRubyを入れる、aptレポジトリを足して新しいnode/npm/yarnを入れるなど、「ふつうのRailsアプリ」を動かすにもいろいろ追加が必要です。そのイメージのビルドは手元で行い、Google Container Registry (GCR - バックエンドはGCS)にdocker pushしてからgcloud app deployする、というのが通常の手順で、つまり2ステップ必要です。

なお、毎回Dockerイメージをビルドして撒くというところから分かると思いますが、稼働中のインスタンスのアプリケーションを「アップグレード」することはできません。新しいバージョンのリリースは、新しいインスタンス(コンテナ)群を起動して、そちらに切り替えるというプロセスになります。(これはHerokuなども同じです)

また、データベースのマイグレーションは誰も自動でやってくれないので、自分で行う必要があります。しかし、gcloud app deployはモノリシックなプロセスで、途中の最適なステージにrails db:migrateを差し挟むということはできません。それではマイグレーションを行った後でデプロイを開始しよう、となりますが、それだと、データベースのスキーマ更新後に長いデプロイプロセスを待つことになります。つまり、少なくとも7, 8分の間、新しいデータベーススキーマの上で古いアプリケーションが動き続けることになり、データベースに不整合が起きやすい状況が生じてしまいます。さて、どうするか…。

マチマチで採用したのは、次の方法です。

  1. 新しいバージョンを、古いバージョンは動かしたまま、プロモーション(トラフィックの切り替え)なしでデプロイする。これはgcloud app deploy--no-promote --no-stop-previous-versionを与えることで可能です。

  2. 新しいバージョンのデプロイを待つ。7, 8分待つと新しいバージョンのインスタンスが起動されますが、データベースのスキーマは古いままなので、「動かない」状態("liveness"チェックは通るが"readiness"チェックは通らない)も想定されます。

  3. データベースのマイグレーションを実行する。

  4. 新しいバージョンの各インスタンスに対し、sudo docker restart gaeappをリモート実行してGAEアプリ用コンテナを再起動する。

  5. 新しいバージョンの各インスタンスが稼働状態になるのを待つ。

  6. トラフィックを古いバージョンから新しいバージョンに切り替える。

  7. 古いバージョンを停止する。

この一連の手順を何とか実装したのですが、まあ面倒でした。4や5は並列処理・待ち合わせが必要ですし、このデプロイ処理が複数同時に走った場合を考え、自分より新しいバージョンが稼働していたら切り替えない、などのチェックも必要です。エラー処理・ロールバック処理をきちんと実装するのも難しくて泣きそうです。

ともあれ、これで古いスキーマ+新しいコードでの稼働状態をだいぶ短くできました。お疲れ様でした。

移行を終えて

なかなか大変な作業でしたが、移行後はほぼ手が掛からず安定稼働しています。ただ、もう少しリソースを自前でコントロールしたい、デプロイを高速化したい、となるとGAEでは限界があるので、そうなる前に、次はGKE (Google Kubernetes Engine)で構築してみたいですね。

なお、コンテンツの大幅な拡充を進めていた今年の6月、突然の負荷上昇と障害に悩まされることになったのですが、そのときの話はまた改めて。

立ち上げ期スタートアップのテスト方針(Ruby on Rails編)

こんにちは!マチマチというご近所SNSを開発している id:fujimuradaisuke です。

スタートアップで働くみなさん、テスト書いてますか?テストの書き方・方針ってチームによってかなり異なるんじゃないかなーと思っています。実際いままで僕が参加したチームもそれぞれに個性がありました。そこでマチマチのテスト方針やその背景をシェアしたら面白いのでは…?と思ったので(僕は他の人のテスト方針を読みたいです)、社内向けにまとめておいた文章を共有してみようと思います。

背景

ご近所SNSマチマチは「ひらかれた、つながりのある地域社会をつくる」というミッションのもと2016年に開発がスタートしました。現在外部のお手伝い頂いている方を含めて5人のエンジニアで開発しています。構成としてはプレーンでモノリシックなRuby on Railsのアプリケーションです。現在は気がつけばモデルが200個とそこそこの規模になっています。フロントエンドはReact.jsを使っていて、コンポーネントは250個ほどという状態です。

目的

  • がんがんリリースできるようにする
  • 「どう動いているべきか」をテストコードとして残す

の2つを目的にテストを書いています。

テストコードがないと新しい変更が正しく動作しているか自信がもてず、リリースへの心理的ハードルがあがってテンポが悪くなってしまいます。また、都度動作確認する時間的コストもバカになりません。

すべての機能に厳密な仕様書を残しメンテナンスするのは現実的ではないので、どうしても動いているコードが正解、ということになってしまいます。しかし動いているコードの現在の挙動は意図しているものなのか?は、テストコードがないとわかりません。自分でも忘れてしまいます。

大方針

  • 複雑にしない
  • End to endテストをがんばる

をモットーにしています。

「複雑にしない」の理由は、複雑さを解決するコストは手を動かすコストより高い、という認識あっての判断です。愚直に手を動かせば直せるものは無理に共通化せず、読みやすい、変えやすい、という状態を目指しています。

具体的には、複数のテストケースで前提を共有しないようにするなど、DRYさより可読性を重視し冗長に書くようにしています。マチマチはRSpecを使っているので、shared_example / shared_contextは極力使わない、beforeやsubjectに無理に共通部分を押し込まないなど。

「End to endテストをがんばる」の理由は、バックエンド・フロントエンド含めたアプリケーション全体としての動作保証で最も重要であるという点が大きいです。マチマチだとfeature specがEnd to endテストにあたります。

feature spec: 重要な仕様の網羅を目指す

全ての表現をfeature specでも網羅することで得られる価値はコストに見合わないと判断していて、「重要な仕様」とは思えない些末なものはテストしていません。具体的にはユーザー体験上進行不能なレベルではないクラスの切り替えによる表示のハイライトなど。こういうところはJavaScriptのテストで書くと簡単な場合もあるので、それもアリです。

feature spec: 長いexampleを書くの推奨

登録プロセスすべて、などを100行あるexampleで一気に書いています。例えば「登録できる」という機能を、メールアドレスとパスワードの送信、プロフィールの入力など、とそれぞれ単体でテストしても組み合わせると失敗する場合もあるので、システムテストとしてはあまり意味がありません。実際に使うときの一連の流れをテストコードとして残す気持ちで書いています。

feature spec: 開発スピード重視

「コメントがn件増えた」ことを検証するときに直接モデルに触ってもOK、としています。End to endテストなので厳密には触ってはいけないはずですが、ここはスピードを重視しました。テストデータの投入もFactoryBotでやっています。

request spec: Web APIのドキュメントのつもりで書く

「期待される挙動を残す」のが目的なので。現状まだドキュメントを生成するまでには至っていません。

request spec: feature specでカバーできていれば書かなくてもよい

副作用、レスポンスともにごく簡単なAPIは書いていません。余談ですが、最近は更新系のAPIで204 No Content を返すことが多くなってきました。面倒だし、レスポンスを使わないんですよね….。

model: Rubyのクラスとしての振る舞いをテストとして残す

ActiveRecord::Baseを継承したモデルもひとつのRubyのクラスです。クラス単体で見てどう振る舞うか、をテストコードして残すようにしています。他のテストでカバーされている場合でも、なるべく単体としての振る舞いはテストケースとして書き残すことを推奨しています。重複に思えるかもしれませんが、拡張、再利用のとき挙動が残されていると楽です。また複数の役割を持ったメソッドってたまに生えてしまうものですが、メソッドの挙動がテストに残っていると不自然さがわかるので、そういったメソッドの発生も避けやすいです。

model: バリデーション、リレーションなど「書けば動く」系のものは込み入ったもの以外書かない

これは過去あまり書いて嬉しかったことがないので書いていません。

job, lib, tasksなどその他

他のテストでカバーできていれば書かなくてもよいが、なるべく書くようにしています。ただ、簡単なジョブ(メソッド呼び出しをバックグラウンド実行するだけのもの)は書かず、feature/request/model specでカバーしています。

まとめ

以上が私達マチマチ開発チームのテストのやり方です。みなさんのチームのテストの方針と比べてどうでしょうか?異論・反論・賛同の声などありましたら、ブコメでもツイートでも直接ご連絡でもよいので、ぜひとも教えてください。何しろ少人数のチームなのでどうしても考えが極化しがちなので、外部の方の意見は自分たちのやっていることを客観視できるのでとてもありがたいです。また、ここはもっと聞きたい、という所があればできる限り追記しますので、遠慮なくお声掛けください。

またマチマチではエンジニアを募集しています。副業も大歓迎です。 藤村武者にお気軽にお声掛けください!