Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20200413前編)最近macOSでRailsが遅い、トランザクションでのreturnやbreakなどが非推奨化、Rails監視ツールリスト2020年度版ほか

こんにちは、hachi8833です。カレンダーのリマインダーで気づきましたが、本当なら今日(注: つっつきが行われた4/10木曜日)からRubyKaigi 2020だったんですね。


つっつきボイス:「そうかRubyKaigiのはずか〜」「もう入学式のシーズン🎓」「子どもの入学式とRubyKaigiとどっち取る問題ってありそうですね」

Rails: 先週の改修(Rails公式ニュースより)

今回もコミットリストから見繕いました。


つっつきボイス:「今の時期はコミットに使える時間増えそう😋」「そういえば今週は珍しく@kamipoさんの姿があんまり見えませんね👀」

.を含むテンプレート名でのレンダリングを非推奨化

# actionview/lib/action_view/template/resolver.rb#L229
      def find_template_paths_from_details(path, details)
+       if path.name.include?(".")
+         ActiveSupport::Deprecation.warn("Rendering actions with '.' in the name is deprecated: #{path}")
+       end
+
        query = build_query(path, details)
        find_template_paths(query)
      end

つっつきボイス:「テンプレート名で.を使って欲しくないということでしょうか?」「.は必ずフォーマット区切りだけに使うということでしょうね: まあ問題ないと思います☺️」「自分も.を名前に含めるのはイヤです😆」

テンプレート名で.の利用を許すと一部で曖昧さが生じる。たとえばindex.html.erbは「テンプレート名がindexでフォーマットがhtml」なのか、それとも「テンプレート名がindex.htmlでフォーマットは指定なし」なのか。人間ならたぶん前者だということはわかるが、Action Viewでindex.htmlをレンダリングすると、Templateで2つの組み合わせが取れることがある: index.htmlがバーチャルパス名になるが、htmlがフォーマットになる。
今回テンプレート名のどこかに.を含むことを非推奨化するにあたり、フォーマット指定用の文字を予約するべき。99%の人はindexではなくindex.htmlを指定するだろう。
実は以前にも3.xシリーズで非推奨化されたことがあり、6c57177で削除された。しかしこの8年間誰もこれを導入していないということを当てにしてよいとは思えない。
同コミットより大意


追記(2020/04/22): その後#38858はDHHによってrevertされました。

new_framework_defaults_6_1.rbでutc_to_local_returns_utc_offset_timesを設定できるよう修正

# railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_1.rb.tt#L27
-# Rails.application.config.active_support.utc_to_local_returns_utc_offset_times = true
+# ActiveSupport.utc_to_local_returns_utc_offset_times = true

つっつきボイス:「長い名前😆」「差分を見るとutc_to_local_returns_utc_offset_timesがActive Supportに引っ越したということみたい」「あ、そういうことですか😳」「Rails.application.configの下にあったのを書き直してますし☺️」

bf34c80の続き。
以前はこのオプションをnew_framework_defaults_6_1.rbで有効にしても反映されなかったのは、railtie初期化がアプリケーション初期化より先に動いていたため。
Active Support railtieに特殊なハンドリングを追加してこのオプションを他より後に適用する方法もあるが、5.0のto_time_preserves_timezoneと同様に直接設定する方が好ましいと考える。
同コミットより大意

「そういえばRailsをrails app:upgradeでアップグレードするときにnew_framework_defaultsなんちゃら.rbというファイルが作られますね」「そうそう、config/initializers/の下に」「どきどきしながら修正してエントリを1個ずつ消していく感じで」「まあ消さなくても残しとけば今までどおりに動いてくれますけど😆」

参考: フレームワークのデフォルトを設定する — Rails アップグレードガイド - Railsガイド

コールバックの:only:exceptに渡した条件を1度だけ算出するようにした

# actionpack/lib/abstract_controller/callbacks.rb#L71
      def _normalize_callback_option(options, from, to) # :nodoc:
-       if from = options[from]
+       if from = options.delete(from)
          _from = Array(from).map(&:to_s).to_set
          from = proc { |c| _from.include? c.action_name }
          options[to] = Array(options[to]).unshift(from)
        end
      end

つっつきボイス:「コールバックの条件は基本的に結果整合になるような形でしか書かないだろうから、こう修正しても大丈夫でしょうね」「バグでしょうか?」「これはバグとは言わないと思います😆」「あ、無駄な処理を減らしたという感じなんですね😅」「テストコードにCallbacksWithReusedConditionsというのがありますけど、only:とかexcept:に渡すブロックは、普通は2回評価しても同じ結果を返すように書きますよね😆」「あ〜😳」「だから『まともな』コールバックを書いている限りはこの挙動が正しいということになると思います😆」「たしかに、呼ばれるたびに結果が違うような条件を書いたらわかりにくくなりそう😅」「逆になぜ今までそうじゃなかったのかというのはありますけど😆」

# actionpack/test/abstract/callbacks_test.rb#L186
+   class TestCallbacksWithReusedConditions < ActiveSupport::TestCase
+     def setup
+       @controller = CallbacksWithReusedConditions.new
+     end
+
+     test "when :only is specified, both actions triggered on that action" do
+       @controller.process(:index)
+       assert_equal "Hello, World", @controller.response_body
+       assert_equal "true", @controller.instance_variable_get("@authenticated")
+     end
+
+     test "when :only is specified, both actions are not triggered on other actions" do
+       @controller.process(:public_data)
+       assert_equal "false", @controller.response_body
+     end
+   end

参照: #38323
修正: #38679
:only:exceptコールバックの条件を1度だけ計算する。
_normalize_callback_optionsはoptionsハッシュを改変するが、only:except:の条件を削除しない。
つまりbefore_actionを同じoptionsハッシュで繰り返し呼ぶと、最終的に完全に同一の:if procや:unless procインスタンスが複数できてしまう。
optionsハッシュは既に_normalize_callback_optionsで改変されているので、この道筋を変えずにonly:except:に渡す条件を削除して続行するよう変更することにした。
別の修正方法として、optionsハッシュが改変される前にdupする手もある。このハッシュはDSLからやって来るので改変しても問題ないと思うが、この別手段で実装するのもやぶさかではない。
同PRより大意

config.force_sslがオンのときにurl_forhttps://をデフォルトで使うようになった

# actionpack/lib/action_dispatch/http/url.rb#L140
          def normalize_protocol(protocol)
            case protocol
            when nil
-             "http://"
+             secure_protocol ? "https://" : "http://"
            when false, "//"
              "//"
            when PROTOCOL_REGEXP
              "#{$1}://"
            else
              raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
            end
          end

つっつきボイス:「Action Mailerは既にそうなっていたので、それをアプリ全体で効くようにしたそうです」「今どきはhttpsでしかホスティングしないのであんまり気にしませんけど😆」「ですよね😆」「今の時代にhttpとhttpsのハイブリッドサイトを新たに構築するとかほぼありえない😆」「つらくなるだけ😆」

Journey::Path::PatternのASTビルドのループを1回にして倍高速化

# actionpack/lib/action_dispatch/journey/path/pattern.rb#L43
        def ast
-         @spec.find_all(&:symbol?).each do |node|
-           re = @requirements[node.to_sym]
-           node.regexp = re if re
-         end
-
-         @spec.find_all(&:star?).each do |node|
-           node = node.left
-           node.regexp = @requirements[node.to_sym] || /(.+)/
+         @spec.each do |node|
+           if node.symbol?
+             re = @requirements[node.to_sym]
+             node.regexp = re if re
+           elsif node.star?
+             node = node.left
+             node.regexp = @requirements[node.to_sym] || /(.+)/
+           end
          end

          @spec
        end

つっつきボイス:「ネストしたループを浅くしたのかなと思ったら、前はfind_allのループを2回走らせてたのを1回にしたのね😳」「これで倍速くなったそうです」「こういう修正は地道に大事☺️」

IPS
Warming up --------------------------------------
                 ast     7.572k i/100ms
            fast_ast    16.195k i/100ms
Calculating -------------------------------------
                 ast     78.133k (± 2.1%) i/s -    393.744k in   5.041539s
            fast_ast    165.113k (± 2.5%) i/s -    825.945k in   5.005666s

Comparison:
            fast_ast:   165112.9 i/s
                 ast:    78132.7 i/s - 2.11x  slower

MEMORY
Calculating -------------------------------------
                 ast   240.000  memsize (     0.000  retained)
                         4.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
            fast_ast    80.000  memsize (     0.000  retained)
                         1.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
            fast_ast:         80 allocated
                 ast:        240 allocated - 3.00x more

returnbreakthrowによるトランザクション終了を非推奨化

このプルリクは以下の記事で知りました。よく見ると2017年のが最近マージされたんですね😳。

# 同記事より
def destroy_post_if_invalid
  Post.transaction do
    post = Post.find_by(id: id)
    return if post.valid?

    post.destroy
  end
end

つっつきボイス:「今まではトランザクション内でreturnするとコミットされてたのか😳」「えぇ!?😳」「そんな書き方普通しないから気にしたことなかったけど😆」

「Saelounブログによると、以下のコミットがRails 3.xのときに入ってたそうです」「トランザクションの中でreturnを書くことがそもそもあるんだろうか?って思いますけど、raiseすることはなくもないかな: 何にしろこれはdeprecateすべきでしょうね🧐」「普通じゃない書き方ですし」「何よりわかりにくいんですよ😅」

「この修正では振る舞いが変わるから、もしそういう書き方をしている人がいたら影響される😆」「そういう人にはbreaking changes😆」「こういう書き方をふんだんにやる人っているんだろうか😆」

# activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L292
      def within_new_transaction(isolation: nil, joinable: true)
        @connection.lock.synchronize do
          transaction = begin_transaction(isolation: isolation, joinable: joinable)
-         yield
+         ret = yield
+         completed = true
+         ret
        rescue Exception => error
          if transaction
            rollback_transaction
            after_failure_actions(transaction, error)
          end
          raise
        ensure
          if !error && transaction
            if Thread.current.status == "aborting"
              rollback_transaction
            else
+             unless completed
+               ActiveSupport::Deprecation.warn(<<~EOW)
+                 Using `return`, `break` or `throw` to exit a transaction block is
+                 deprecated without replacement. If the `throw` came from
+                 `Timeout.timeout(duration)`, pass an exception class as a second
+                 argument so it doesn't use `throw` to abort its block. This results
+                 in the transaction being committed, but in the next release of Rails
+                 it will raise and rollback.
+               EOW
+             end
              begin
                commit_transaction
              rescue Exception
                rollback_transaction(transaction) unless transaction.state.completed?
                raise
              end
            end
          end
        end
      end

Rails

enumerize: i18n対応のenum

enumerizeはだいぶ前のウォッチで一度取り上げましたが、以下のおたよりを見かけたので。


つっつきボイス:「enumerizeはどこかで使ったことあった気がする🤔」「i18nに対応したenumということですね」「誰もが考えるヤツ😆: Railsでi18nに対応していない部分はちらほらあるので、そういう部分をこういうgemでサポートしたいでしょうね☺️」

「enumrizeを使うということは、要するにRails標準のenumを使わないということね: 自分もRails標準のenumは使いたくない派なのでワカル😆」「クエリビルダの汚染というのがまだイメージできてなくて😅」「自分もそこの意図はよくわかりませんが😆、Active Recordのenumを使うとシンボルとかをenum値に変換するので、ソースコードと実際のSQLを比較したときにわかりにくいコードになるのは確か😭」「やりにくそう…😅」

参考: ActiveRecordのenumで気をつけたい3つのポイント - Misoca開発者ブログ

「次のツイートも見つけました」「別テーブルと外部キー、個人的にはあんまりやらないかな: なおDBアドミニストレータ(DBA)の立場ならenumにする方がインデックスサイズは小さくなるから当然速くなるというのはありますね☺️」「ふむふむ」

「PostgreSQLならデータベースレベルでenum型があるので、やるならそっちを使う方がいいかなとちょっと思います😋」

参考: 【Rails】5ステップでイケてる enum を作る(翻訳) - Qiita

RailsがmacOSだと最近遅いのはなぜ?(Ruby Weeklyより)


discuss.rubyonrails.orgより

samsaffronさんによると、ここ1年Mac上でRails specのベンチがLinuxネイティブやWSL2と比べてやけに遅いとのことです。MacのDocker上だとさらに遅いようです。


つっつきボイス:「やっぱりMacだと遅いのか😢」「今どきはほとんどの人がDockerでやってる気がしますね🐳」「DockerもMacだと遅いという説もありますし」「ファイルシステムアクセスが特に遅いですね🐢」

「MacのファイルシステムがLinuxと違うために、ファイル変更を検出して自動リロードするのがMacでうまくやれないのが悔しいです😭」「MacでDockerやるとHypervisorフレームワークの上でLinuxを動かすことになるのでそうなっちゃいますね☺️」「くすん😢」「そこを何とかするには、Hypervisorフレームワークを通して、Linuxで言うinotify的なMacのファイル更新通知を伝搬させないといけないんですよ🧐」「やっぱりそうなるんですね」「やってやれないことはないんでしょうけど、いろいろ面倒そう😅」「どうしてもダメならダサくpollingするしかないのかな…」「まあpollingは最終手段でしょう😆」

参考: inotify-toolsでファイルやディレクトリを監視する - Qiita
参考: ポーリング (情報) - Wikipedia

Docker for MacのEdgeリリースノートを見る限りでは、inotify周りがまだ不安定な印象です。

参考: Docker Desktop for Mac Edge release notes | Docker Documentation


「ところで、公式にこういうディスカッション場↓があることを今になって知りました」「へぇ知る人ぞ知る公式😳」「discussionというサブドメインがあるとは」「見た感じそんなにアクティブという感じではなさそうですけど😆」「初歩的な質問にピンポイントでレスが付いておしまいになってるのも多いみたいです」「まあ気軽に質問できそうでいいかも😋」


discuss.rubyonrails.orgより

indexページをページネーションしないとRailsが死ぬかも(Ruby Weeklyより)


同記事より


つっつきボイス:「ページネーションしないindexアクションはだいたい死にます、以上😆」「😆」「ものにもよりますけど普通にメモリ溢れたりしますし☺️」


記事概要:

  • Rails+Pumaが突然メモリを激しく消費し、20分おきにPumaプロセスがkillされた
  • メトリクス
  • 調査に便利なコマンド技
  • ページネーションしてれば問題はたぶん起きなかった
  • Pumaにはタイムアウトオプションがない(たぶん今後も)
  • 以下のバグを発見(id=nilで問題が発生する)
    • 今後のためにSorbetを入れようとしたけどレガシーアプリにつき無理だった
# 同記事より
class FeatureRepository
  def self.find(id)
    OtherApp::Clients::V1::Feature.find(id)
  end
end
  • ソースをロールバックすればもっと早く発見できたかも

Rails向け監視ツールリスト2020年度版(Ruby Weeklyより)


つっつきボイス:「いわゆるAPM(Application Performance Monitoring)ツール」「こうして見るとずらっとありますね」「エラー/例外監視なんかもあったりして範囲広いな〜」

「AirBrakeは無料トライアルもあるけど基本的には有料: AirBrakeのオープンソース版がerrbit↓」「そういえば以前も話題になりましたね(ウォッチ20170804)」「BPSでもかなり昔からerrbit使ってます😋」

Railsのエラー管理はこれでOK!オープンソースのAirbrakeクローン、errbitを使ってみた

「コメント欄に『Scout APMも載せるべき』とありますね」「今は『Rails APM』で検索すればScout APMが即出てきますヨ☺️」


scountapm.comより

optimism: Rails用リモートバリデーションgem(Ruby Weeklyより)


つっつきボイス:「デモサイトがそっけないですね😆」「Ajaxでやってるのかなと思ったら、ChromeのDevToolsで見るとどうやらAction Cableでやっているらしい↓」「ホントだ😳」

「つまりページ遷移する前にAction Cable経由でバリデーションを走らせてるんでしょうね🧐」「なるほど!」「このぐらいのバリデーションをgemでやるかどうかというのはありますが😆」「★も70個ぐらいだし割と新しいgemみたい」「productで使ってる人はまだ作者ぐらいかな?😆」

後で調べると、optimismでは以下のcable_readyというgemを使っていますね。

その他Rails

つっつきボイス:「新しい記事なんですがform_withがかけらも登場していなくておや?と思ったので」「form_forで書くのはさすがに古い😆」「ですよね😆 」「相当古いRailsをメンテしてるのかな?ネステッド周りは基本的にやり方変わってないからまあいいでしょう☺️」

参考: Rails5.1からのform_withでnested_formを扱う方法 - Qiita

Rails 5.1〜6: ‘form_with’ APIドキュメント完全翻訳


前編は以上です。

バックナンバー(2020年度第2四半期)

週刊Railsウォッチ(20200407後編)RubyのTracePointでデバッグ、Rubyとモナド、Gitノウハウ集、リモートワークほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。