RubyKaigi Takeout 2021 に登壇します

RubyKaigi Takeout 2021 に『RuboCop in 2021: Stable and Beyond』というタイトルで登壇します。

私の登壇は1日目である 2020年9月9日(木) の 11:30-11:55 です。

rubykaigi.org

前回の RubyKaigi Takeout 2020 での講演が RuboCop 1.0 リリース前の道のりだったことに対して、今回は RuboCop 1.0 リリース後の動きと今後に向けたトピックでまとめています (絶賛準備中) 。開発の舞台裏についても少し取りあげる予定なので、人によっては OSS 開発環境の参考になったりするかもしれません。よければテイクアウトしに来てください。

DatabaseCleanerを使ったテストを倍速にした設定の見直し

はじめに

テストのデータベースクリーニングをする gem としては、DatabaseCleaner と DatabaseRewinder がメジャーどころとしてあります。

github.com

github.com

今回は DatabaseCleaner の話です。

クリーニング戦略を切り替えて計測する

DatabaseCleaner では transaction, deletion, truncation といったデータベースのクリーニング戦略があり、E2E での非同期 UI 系のテストでは transaction 以外を選択することになると思います。

早速結論ですが、非同期 UI 系のスローテストについて truncation から deletion に変えたところ 60 分の CI が 30 分になりました。

     if example.metadata[:javascript]
-      DatabaseCleaner.strategy = :truncation, {except: MASTERS}
+      DatabaseCleaner.strategy = :deletion, {except: MASTERS}
     else
       DatabaseCleaner.strategy = :transaction
     end

RDBMS によると思うのですが、truncation は (deletion に比して) 概ね固定された実行効率であることに対して、deletion はデータ量に依存したり変動要素が多いようです。 そしてこれらは、どちらが優れているというものではなくコンテキストによって選択されることになります。

詳しくは DatabaseCleaner から参照されている Stack Overflow の記事を参照してください。

stackoverflow.com

テスト時では閾値とを INSERT するデータ量を小さくするなどの定石があったりするため、テストの作りや RDBMS によって deletion の方が速いことがあるというのはなるほどというものでした。長年開発を続けてスキーマやデータが変遷しているようなアプリケーションは一度計測し直してみて良いかもしれません。

まとめ

実際のところ、どちらが速いというよりはコンテキストによるようなので、テストが遅いなあと思ったら、戦略を切り替えてみるとテストが速くなることがあるかもしれません (し遅くなるかもしれません) 。CI の結果を計測してみましょう。

マジかというものですが、事実のみを示す数字によるとデータベースクリーニングで 30 分くらい余計にかかっていたわけでした。

最後に過去発表した関連スライドのページをのせておきます。

www.slideshare.net

今日はここまでです。ハックを続けましょう。

社内向けに「Rubyコミュニティの歩き方勉強会」を行った

2年くらい前に Rails Developers Meetup 2019 で登壇したときのスライドを使って社内向けに開催した。

コロナ禍でオフラインでのコミュニティ活動というものが休止の中で、オンラインでのあり方を再度模索するきっかけになっていればと思う。

いっぱい話した気がするけれど、「アウトプットは人とのつながりを生む」という話だったと ima1zumi メンバーが日報でワンフレーズにまとめてくれた (さすがのまとめ力!) 。要はそういうことを今回伝えたかったんだなという 2021年春のお気持ちだった。

参加メンバーからは、アウトプットへの機運が高まったという反応をもらえたので、アウトプットによって新たなアウトプットを生むきっかけになったというあたりに、開催の意義があったんじゃないかなと思う。

私は引き続き日々のパッチがアウトプットの中心です。月一で Rails/OSS パッチ会というものを勤務先からオープンに主催しているので、機会があればオンラインミートアップでお会いしましょう。

RuboCopファミリーと周辺の更新 (GW 2021編)

GW に更新のあった RuboCop 周辺のアップデートについてざっくりまとめておく。

Parser 3.0.1.1

github.com

RuboCop が依存する Ruby構文解析の Gem で、Ruby 3.0 で入った endless method definition のバグ修正と Ruby 3.1 (dev) のサポート追加をしている。

Ruby 3.0 で導入された endless method definition は以下のように = で終わる writer メソッドは定義できない。

% ruby -cve 'def foo=(foo) = @foo = foo'
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin19]
-e:1: setter method cannot be defined in an endless method definition
def foo=(foo) = @foo = foo

それについて method_name.end_with?('=') でマッチさせていたので、受け入れられるべき比較演算子 (e.g. ==) となるメソッド定義までエラーにしていたという Parser gem のバグだった。

Ruby 3.1 (dev) のサポートについては、パターンマッチの新構文となる pin オペレータをサポートするようになった。大元のサンプルから持ってくると ^ を使った式をこんな感じでパターンマッチ中で取れるようになります。夢がありそうな拡張。

Prime.each_cons(2).lazy.find_all { _1 in [n, ^(n + 2)] }.take(3).to_a
#=> [[3, 5], [5, 7], [11, 13]]

RuboCop 1.4

github.com

Ruby 3.1 (dev) をサポートしました。また、Bundler/GemVersion cop と Layout/SingleLineBlockChain cop がデフォルト無効で追加されています。

Bundler/GemVersion は Gemfile に gem 指定するときにバージョンを必須にする cop だけれど、理由がなければバージョン指定するべきではないという見解でデフォルト無効です。個人的にはデフォルト無効ならまあ (コアに入れて) いいか。。。という気持ちです。 Layout/SingleLineBlockChain cop は、example.select { |item| item.cond? }.join('-') のようなチェーンを許可しない cop で、ふつうに厳しすぎるのでデフォルト無効は妥当だと思って見ていました。余談だけれど blockchain という用語は暗号資産を連想したという bbatsov のコメントがおもしろかった。

あと、RuboCop AST とそれが依存する Parser の依存バージョンを上げているので、bundle update --conservative rubocop でアップグレードしているようであれば、bundle update --conservative rubocop rubocop-ast parser あたりの依存 gem 指定をしないと 1.14 に上がらないかもしれません。

RuboCop AST 1.5

github.com

Ruby 3.1 (dev) をサポートしています。

RuboCop Performance 1.11.3

github.com

Performance/MapCompact cop の不具合修正をして v1.11.3 まで上がりました。あと互換のない compact.mapfilter_map に手動で置換されようとしたケースを rails/rails で見かけたので、それは検出しない旨をドキュメントとテストに追記しています。

RuboCop Rails 2.10

github.com

RuboCop Rails は半年くらいぶりのリリース。古めのイシューなんかもさらって、フィードバックにもとづいた cop の安全性などの見直しを入れており、2.9 系を調整した better バージョンな感じになっていると思います。RuboCop Performance 1.11 系と同様に、Ruby 2.4 のサポートを打ち切ったのと、今後のメジャーアップデートに向けて RuboCop 1.7.0 以上への依存に更新しています。古めのバージョンを使っているようであれば、あわせてアップデートが必要です。

ご活用ください。

RuboCop Performance 1.11 をリリースした

RuboCop Performance 1.11 をリリースした。主な変更点は以下。

  • Ruby 2.4 のサポートを終了した
  • Ruby 2.7 で追加された Enumerable#filter_map への Cop を追加した

github.com

前者はそのままなので、後者について記す。

Enumerable#filter_map に対応づけられるのは、以下の2つの Cop となる。

これは当初 Performance/FilterMap cop として提案していたものだが、対象となる bad ケースのコンテキストや good ケースとの互換性に違いがあるため、別々の Cop として分割した。

Performance/SelectMap cop

github.com

この Cop は select.map のチェーンへのケースを filter_map に置き換えることを提示する。

# bad
ary.select(&:foo).map(&:bar)
ary.filter(&:foo).map(&:bar)

# good
ary.filter_map { |o| o.bar if o.foo }

振る舞いに互換性はあるが、good ケースの可読性について賛否があるのと、複雑なブロック処理については自動修正が困難だと思い自動修正を提供しておらず、手修正も大変そうということからデフォルトで無効にしている。

Performance/MapCompact cop

github.com

この Cop は map.compact のチェーンへのケースを filter_map に置き換える。このケースは good ケースになることでコードもシンプルになると思う。

# bad
ary.map(&:foo).compact
ary.collect(&:foo).compact

# good
ary.filter_map(&:foo)

以下のいずれのケースも nil を除外するが、filter_map は加えて false も除外する。このように compactfilter_map は振る舞いに互換性がないため、SafeAutoCorrect: false (rubocop -A で適用) としている。

[true, false, nil].compact              #=> [true, false]
[true, false, nil].filter_map(&:itself) #=> [true]

なお、Enumerable#filter_map! というメソッドは存在しないので、以下のケースは受け入れている。

ary.map(&:foo).compact!

Performance/SelectMap cop は好みが分かれるところだと思うが、Performance/MapCompact cop は適用可能であれば適用しておくとコードとしてもシンプルになると思う (ただし振る舞いの違いに気をつけて適材適所で) 。

いつもの新規 Cop のようにデフォルトで pending ステータスなので、NewCops: enable にしているわけでなければ、以下のように有効化が必要となる。

# .rubocop.yml
Performance/MapCompact:
  Enabled: true

今日はここまでです。ハックを続けましょう。

RuboCop Oracleをリリースした

RuboCop Oracleを実装してリリースした。

github.com

長年 RDBMSOracle (Active Record Oracle enhanced adapter) を使った運用をしているが、これは無停止リリースを行うにあたってのマイグレーションの tip を cop にして含んでおいたもの。

最初の v0.1.0 リリースで含んでいるのは Oracle/OnlineIndex cop 単品。これは CREATE INDEX の際に ONLINE オプションをついていないマイグレーションファイルを検知する cop です。

docs.oracle.com

Oracle/OnlineIndex cop

データベースのインデックス追加を行う際は ONLINE オプションを付けないと OLTP (オンライントランザクション処理) が有効にならず、オンライン稼働のままインデックス作成を行おうとするとオンライン処理とそれぞれ処理待ちになりうる (結果としてオンラインサービス影響が起きうる) 。

この Oracle/OnlineIndex cop の bad と good は以下。options: :online を付与することで OLTP が有効になる。

# bad
add_index :table_name, :column_name

# good
add_index :table_name, :column_name, options: :online

マイナス面としては OLTP を有効にしたときよりも処理が遅くなるといったことがありうるが、無停止でサービス影響なしで進めるのを第一に ONLINE オプションを付けておこうという判断をするプロジェクトには有効だと思う。

また Oracle/OnlineIndex cop のオプションとして MigratedSchemaVersion というものがある。たとえば、RuboCop Oracle 導入以前に db/migrate/202104130150_add_title_index_to_articles.rb まで本番環境に適用済みであれば、それ以前のマイグレーションファイルへの ONLINE オプションの付与を検知したくない場合に、適用済みのマイグレーションバージョンを記すことで、適用済みのマイグレーションファイルへの検出を防ぐことができる。

Oracle/OnlineIndex:
  MigratedSchemaVersion: '202104130150'

git による並列開発で、必ずしもマイグレーションバージョンのタイムスタンプが過去から未来へと流れているわけではないが、だいたいはうまくいくだろう。bin/rails db:migrate:status あるいはその内部 API を使えばさらに誤検知を防げるだろうが、静的解析の域を超えるので実装はしないことにしている。そのあたりはコードレビュー掛け合わせになるだろう。

開発雑記

RuboCop Oracle の開発構想自体はずいぶんと前からあって、一番の悩みは名前だった。Active Record Oracle enhanced adapter をもう少し前面に出した名前にするか悩んだが、gem で公開するときの名前をシンプルにしたかったのと、gem + RuboCop の世界で Oracle を冠して困るようなことはなかろうとやや大きめの名前だが RuboCop Oracle にした。 また、RuboCop Rails に含めた cop にすることも少しだけ考えたが、Oracle adapter 特化なので独自の gem にした。

そのほか、RuboCop Rails でもマイグレーションファイルへのルール検知をしたいという PR がいくつかあるが、マイグレーションファイルは適用済みかどうかの観点も重要なため取り込めていないものがある。今回の MigratedSchemaVersion はその課題に対するひとつの習作となっている。デフォルトで db/schema.rb のスキーマバージョンを活用するなど、もう少しリッチにするかもしれない (など、まだ改善の余地がある) 。

あとプロダクト自体のドキュメンテーションはもう少し整えておく予定。

Oracle x Rails でのサービス運用をしているプロジェクト向けですが、ご活用ください。

Ruby 3.0.1, 2.7.3, 2.6.7, 2.5.9 がリリースされた

Ruby 3.0.1, 2.7.3, 2.6.7, 2.5.9 がリリースされた。安定版へのメンテナンス、リリースありがとうございます。

脆弱性対応

Ruby 3.0 系から、このたび EOL になった 2.5 系まで含めると、以下3つの脆弱性への対応となるようです。

使っている Ruby のバージョンと環境によって対象となる脆弱性が異なるため、各自ひととおりのリリースと CVE アナウンスを見て対処すると良いと思います。

Ruby 2.7.3 への arguments forwarding 構文への拡張のバックポート

Ruby 2.7.3 には Ruby 3.0 から arguments forwarding 構文への拡張のバックポーティングがありました。以下のように ... の前の引数にも対応します。

% cat example.rb
def foo(arg1, arg2, ...)
  puts('## arg1')
  puts(arg1)

  puts('## arg2')
  puts(arg2)

  puts('## ...')
  puts(...)
end

foo('foo', 'bar', 'baz', 'qux')

% ruby example.rb
## arg1
foo
## arg2
bar
## ...
baz
qux

Parser gem に対応 PR を開いておいたので、近いうちに RuboCop でも解析可能になる予定です。

github.com

2021年4月6日同日追記

Parser gem 3.0.1.0 としてリリースされました。ご活用ください。

rubygems.org