Ruby Banitsa THURSDAY, 21ST OF MAY

昨晩というか今日の JST 1:30 くらいから Bozhidar が RuboCop 1.0 の話をするというのでインターネット講演を視聴していた。

rubybanitsa.com

たぶんブルガリア方面のローカル Ruby コミュニティのようで、ツールは Zoom で 34 人くらい参加していたと思う。 ブルガリア語 (?) の講演だったっぽいところ、Genadi から英語に切り替えようか?とチャットが来たけれど、スライドは英語だったのでそのまま続けてもらって視聴していた。あと講演が終わった後のチャットがブルガリア語っぽくて、それはそれで新鮮な世界だった。もちろん読めたりはしないけれど。

他にも Palkan とか参加していたように思えたけれど、みんな元気そうでよかった。

話としては RubyKaigi 2018 で Bozhidar が来日講演した内容に RuboCop 1.0 で実現されていることの話がミックスされていて、自分が RubyKaigi 2020 で話そうとしていたこととオーバーラップしている部分もあった。これはどこかでまとめて話すと思うので割愛。

ともあれ地球の裏側のテックトークに瞬時に参加できて、インターネットは本当にえらいと思った。

あと、RuboCop 1.0 は来週、再来週にはリリースされるんじゃないかなと思う。

自分としてはもういくつかやりたいことがあったけれど、時間は有限なので RuboCop 1.0 以降に持ち越しです。とりあえずコントリビュータ時代から足掛け 3年半越しでの LineLength のデフォルト値を 80 から拡張できたので、1.0 の目玉になるもののひとつはできたんじゃないかなと。

このあたりの話に興味があれば、来月に勤務先の Rails/OSS パッチ会をオンラインで再開するのでよければどうぞ。

blog.agile.esm.co.jp

Ruby 2.7 と Rails 6.0 / Rails 5.2 への展望と懸念

Ruby 3.0 に向けた Ruby 2.7 と Rails の状況がめまぐるしく変わっているようなので、現在持っている知識のスナップショットを書き残しておきます。

Rails 6.0 系と Ruby 2.7 系

昨日、Matz によって今後リリースされる Ruby 2.7.2 でキーワード引数の分離への警告出力が Ruby 本体側で取りやめになるだろう旨の投稿が Rails の Discuss フォーラムにありました。

discuss.rubyonrails.org

これにより以下の組み合わせでは、キーワード引数の分離への警告が出なくなる見通しが考えられます。

というか警告が抑制される場合の Ruby 2.7.2 はジョーカーなので、Rails 6 系での Ruby 2.7 普及バージョンになる気がしています。

Rails 5.2 系と Ruby 2.7 系

さて、Ruby 2.7.2 でキーワード引数の分離への警告が出なくなったので、Rails 5.2 系ユーザーについても Ruby 2.7 に気兼ねなくアップグレードできるかというと、Ruby 2.7 で入った Monitor の非互換が Rails 5.2 系にはいまのところバックポートされていないようで、これがどのようなことを招くのかが (私は) よく分かっていません。

いちおうこのことについては、勤務先の Idobata をとおして amatsuda 顧問に伝えたので、何かしらの動きがあるかもしれません。

既存の Rails 5.2 系ユーザーが取れる手順としては、Rails 6.0 にアップグレードしてから Ruby 2.7 系にアップグレードするというのが、現状のリスクを減らした移行手順になりそうです。

Rails 5.1 系以下

Rails 5.1 以下はすでにサポートされていないので、Rails 5.2 系以上にアップグレードしましょう。

guides.rubyonrails.org

(Ruby 2.7 系と Rails 5.2 系の組み合わせへの懸念については、yahonda さんに教えて頂きました。)

Railsでcontent_tagメソッドの代わりにtagメソッドに使う

Rails で content_tag メソッドの代わりに tag メソッドに使うように促す cop を次の RuboCop Rails 2.6.0 で導入する予定です。

github.com

以下、bad ケースと good ケースをサンプルから抜粋します。

# bad
content_tag(:p, 'Hello world!')
content_tag(:br)

# good
tag.p('Hello world!')
tag.br

2020年5月14日追記

第一引数に変数をとるケースについては、public_send を使うくらいなら content_tag を使う方が良さそうです。

以下は元記事として残していますが、これらの方法はとらない方が良いです。


PR の説明にあるように content_tag はレガシーな API ということで、デフォルトで有効で良いと思っているものの、現状だと第一引数に変数をとる以下のケースに問題があります。

オートコレクトでコードが壊れることを防ぐため、以下の PR を問題解決のため開いていますが public_send を使っているのが気にかかっている点です。

github.com

Case 1: NoMethodError を防ぐ

元コード:

content_tag(name, 'foo', class: 'bar')

現状の auto-correct:

tag(name, 'foo', class: 'bar')
#=> NoMethodError (undefined method `each_pair' for "foo":String)

今後:

tag.public_send(name, 'foo', class: 'bar')

Case 2: symbolize_keys を使って ArgumentError を防ぐ

元コード:

content_tag(name, 'foo', {'class' => 'bar'})

現状の auto-correct:

tag.public_send(name, 'foo', {'class' => 'bar'})
#=> `ArgumentError (wrong number of arguments (given 3, expected 1..2))`

今後:

symbolize_keys を使って ArgumentError を防ぐハックをしている。 引き渡されたハッシュのキーがすべてシンボルかどうか静的解析で検知がつらいための苦肉の策。

既存アプリケーションが壊れるのを防ぐためとはいえ、この解決はつらい。

tag.public_send(name, 'foo', {'class' => 'bar'}.symbolize_keys)

Case 3: ArgumentError を防ぐため三項演算子を使っている

元コード:

content_tag(name, 'foo', options)

現状の auto-correct:

options = nil
tag.public_send(name, 'foo', options)
#=> `ArgumentError (wrong number of arguments (given 3, expected 1..2))`

optionsnil のときに ArgumentError です。

今後:

tag.public_send(name, 'foo', options ? options.symbolize_keys : {})

なかなか厳しいものがあるので、content_tag の第一引数が変数の場合は無視するという対応も考えられるものの、もっと良い tag への変換があれば教えてもらえると嬉しいです (知らないだけでもっと良い tag メソッドへの置換方法がある?) 。

Faker のコミット権をもらった

昨年くらいに Faker の org メンバーになっていたけれど、リポジトリの PR がたまっていている状態をどうにかしたかったので、そこのチームにコミット権を付与してもらった。

github.com

アクティブなチームメンバーがマージできるようになったのではと思う。

GW にやったこと (2020年)

GW にやったことについて、GitHub から辿れることを中心に書き残しておく。

.rubocop.yml で ERB を書けるようになるパッチをレビューしてマージした。

こんな書き方ができるようになる。

AllCops:
  Exclude:
  <% `git status --ignored --porcelain`.lines.grep(/^!! /).each do |path| %>
    - <%= path.sub(/^!! /, '') %>
  <% end %>

github.com

Active Record Oracle enhanced adapter に対する upstream の日次動向チェックで動きがあったので追随した。

github.com

以前から気にはなっていた Active Record Oracle enhanced adapter のテスト時の警告を抑制した。

github.com

.rubocop.yml で無効になっている部署に対して、一部の cop を有効にできるようにするパッチをレビューしてマージした。べんり。

github.com

Layout/ConditionPosition cop の auto-correct を実装した。

github.com

以下のようにマルチバイト文字への振る舞いが等価でないため Performance/Casecmp cop を unsafe にした。

'äöü'.casecmp('ÄÖÜ').zero? #=> false
'äöü'.casecmp?('ÄÖÜ')      #=> true

github.com

レガシー APIcontent_tagtag にする Rails/ContentTag cop をレビューしてマージした。PR の根拠となるリソースなど示されているためレビューの手間が少なくなっていたのでよかった。

ドッグフーディングとして実 Rails アプリケーションに適用してみて良い感じだった。

2020年5月7日追記

偽陽性がありそうだったので、調べるつもり。

github.com

Style/GuardClause cop の誤ったメッセージを修正した。

github.com

Lint/EnsureReturn cop の auto-correct を実装した。

github.com

RailsConf 2020.2 COUCH EDITION の視聴をはじめた。往年の Kent Beck ファンとして濃密な 15min の講演がよかった。

railsconf.com

rubocop-hq/rubocop#7925 が複合要因のイシューで未解決部分があった Style/GuardClause cop の偽陽性を修正した。

github.com

Performance/DeletePrefix cop と Performance/DeleteSuffix cop を実装した。

bad ケースと good ケースは以下。

# bad
str.gsub(/\Aprefix/, '')
str.gsub!(/\Aprefix/, '')
str.gsub(/^prefix/, '')
str.gsub!(/^prefix/, '')

# good
str.delete_prefix('prefix')
str.delete_prefix!('prefix')

ベンチマークは以下。

% ruby -v
ruby 2.5.8p224 (2020-03-31 revision 67882) [x86_64-darwin17]

% cat bench.rb
require 'benchmark/ips'

Benchmark.ips do |x|
  str = 'foobar'

  x.report('gsub')           { str.gsub(/bar\z/, '') }
  x.report('gsub!')          { str.gsub(/bar\z/, '') }
  x.report('delete_suffix')  { str.delete_suffix('bar') }
  x.report('delete_suffix!') { str.delete_suffix('bar') }
  x.compare!
end

% ruby bench.rb
Warming up --------------------------------------
                gsub    46.814k i/100ms
               gsub!    46.896k i/100ms
       delete_suffix   211.337k i/100ms
      delete_suffix!   208.332k i/100ms
Calculating -------------------------------------
                gsub    546.500k (± 1.3%) i/s -      2.762M in 5.054918s
               gsub!    551.054k (± 1.2%) i/s -      2.767M in 5.021747s
       delete_suffix      4.780M (± 1.1%) i/s -     24.092M in 5.040850s
      delete_suffix!      4.770M (± 1.1%) i/s -     23.958M in 5.022823s

Comparison:
       delete_suffix:  4780060.8 i/s
      delete_suffix!:  4770419.3 i/s - same-ish: difference falls within error
               gsub!:   551054.2 i/s - 8.67x  slower
                gsub:   546500.1 i/s - 8.75x  slower

ドッグフーディングとして rails/rails リポジトリと、実 Rails アプリケーションで実行してみて良い感じの首尾だった。

github.com

Rails 6.0.3 がリリースされたので、ホームページを更新した。

github.com

GitHub 社の Codespaces のベータの waitlist にならんだ。

github.com

まだ PR にしていないセルフレビュー中のパッチや、仕掛かりや手戻りのパッチなど手元にあるので、それらは今後引き続きといった感じ。

カンファレンスとしては、去年のゴールデンウィークは RailsConf 2019 のためミネアポリスに行っていたけれど、RailsConf 2020 はオンラインで聞くゴールデンウィークだった。

Active Recordでのヒント句の書き方

Active Recordでのヒント句の書き方について。

クエリの実行計画の最適化を RDBMSオプティマイザ (プランナ) に任せずに、アプリケーション側で指定するのにヒント句というのがあります。通常 RDBMSオプティマイザに任せたりしていますが、DBA からチューニングのアドバイスがあったりしたときに使ったりできます。

OracleMySQL 5.7.7 以上 (MariaDB 除く) 、PostgreSQL だと pg_hint_plan が使えるものあたりがサポートしているようです。ヒント句は既存のクエリ自体は書き換えることなく、コードコメントで指示を出せるのが売りのようです。

SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`

Active Record で書く際もなるべく既存のクエリ API に手を入れない形で書けると嬉しいところ。

Rails 6.0 以上だと、kamipo さんのパッチによって optimizer_hints メソッドを使った指定ができます。メソッドチェーンで足す形で書けるのが便利。以下 API ドキュメントから抜粋。

Example (for MySQL):

Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
#=> SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`

Example (for PostgreSQL with pg_hint_plan):

Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
#=> SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"

github.com

Rails 5.2 以下であれば、select メソッドを使って指定するといった指定なんかになると思います。 以下は select の振る舞いを利用して、カラムの前にコメントとしてヒントを足す方法ですが、既存の select メソッドの引数に手を加える必要があるのが難点です。

Topic.select("/*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.*")

他にも方法はあると思いますが、Rails 6.0 以上でのoptimizer_hints メソッドが使えれば流れるようなインタフェースにできてクールでしょう。

bliki-ja.github.io

2020-04-21 追記

PostgreSQL へのサポートについて kamipo さんからのアドバイスを追記しておきます。

`bundle install --clean`を実行するときの注意点

bundle install --clean を実行するときの注意点として、バッドノウハウを書き残しておきます。

これは、以前 pocke さんが CircleCI の実行を速くするのに bundle install --clean を使う方法を記されていたことで思い出したことです。

pocke.hatenablog.com

bundle install --clean は Bundler 管理下で不要な gem とそのバージョンを削除します。pocke さんの伝える CI 上の gem のキャッシュを削除する用途以外に、イメージ化していないデプロイ環境で bundle update している環境なんかでも同一 gem に対して複数バージョンがインストールされていたりします。そういった環境で bundle install --clean を実行すると、不要バージョンを削除してガツっと容量を空けることができたりします。

以上を踏まえて、本題の注意点です。問題となるのは Gemfile に記載せずにサーバーに直接 gem install passenger とか gem install unicorn といった感じでデプロイ環境に直接サーバーをインストールして、Gemfile / Gemfile.lock にサーバーの gem が記されていないケースです。その状態で bundle install -clean するとサーバーが消えます。文字列に起こすといかにも間抜けですが、やらかすと悲しいことになるというか、かつてなったことがあるのでここに記しておきます。Unicorn で書き忘れはない気がしますが、Passenger なんかは Apache モジュールとしてコンパイルしたりした後で、うっかり Gemfile に書き忘れていたりしないか気をつけると良いです。

そうそう踏む人はいないと思いますが、サーバー含めてインストールする gem はきちんと Gemfile に記しておきましょうという話でした。