- Ruby / Rails関連
週刊Railsウォッチ(20200413前編)最近macOSでRailsが遅い、トランザクションでのreturnやbreakなどが非推奨化、Rails監視ツールリスト2020年度版ほか
こんにちは、hachi8833です。カレンダーのリマインダーで気づきましたが、本当なら今日(注: つっつきが行われた4/10木曜日)からRubyKaigi 2020だったんですね。
末娘の入学式に出れたのはRubyKaigi中止されたたった一つの良いことだなあ。
— Yukihiro Matzmotto (@yukihiro_matz) April 9, 2020
つっつきボイス:「そうかRubyKaigiのはずか〜」「もう入学式のシーズン🎓」「子どもの入学式とRubyKaigiとどっち取る問題ってありそうですね」
⚓Rails: 先週の改修(Rails公式ニュースより)
今回もコミットリストから見繕いました。
つっつきボイス:「今の時期はコミットに使える時間増えそう😋」「そういえば今週は珍しく@kamipoさんの姿があんまり見えませんね👀」
⚓.
を含むテンプレート名でのレンダリングを非推奨化
- PR: Deprecate rendering templates with . in the name by jhawthorn · Pull Request #38858 · rails/rails
# 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されました。
- PR: Revert "Deprecate rendering templates with . in the name" by dhh · Pull Request #39012 · rails/rails
⚓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_for
でhttps://
をデフォルトで使うようになった
# 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回にして倍高速化
- PR: Build Journey::Path::Pattern ast in a single loop by vinistock · Pull Request #38901 · rails/rails
# 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
⚓return
やbreak
やthrow
によるトランザクション終了を非推奨化
このプルリクは以下の記事で知りました。よく見ると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すべきでしょうね🧐」「普通じゃない書き方ですし」「何よりわかりにくいんですよ😅」
- commit: Fix transactions so that calling return while inside a transaction wi… · rails/rails@fc83920
「この修正では振る舞いが変わるから、もしそういう書き方をしている人がいたら影響される😆」「そういう人には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
- リポジトリ: brainspec/enumerize: Enumerated attributes with I18n and ActiveRecord/Mongoid support
- 元記事: 【Rails】enumをさらに便利にしてくれるgem enumerize(日本語化も) - おぴよの気まぐれ日記
enumerizeはだいぶ前のウォッチで一度取り上げましたが、以下のおたよりを見かけたので。
Rails標準のenumはクエリビルダの汚染がひどいので、絶対使わないですね
あと配列じゃなくてハッシュ定義も大事
enumerizeサイコー!週刊Railsウォッチ(20200406前編)Ruby 2.7.1セキュリティ修正、RailsビューHTMLにテンプレート名を出力ほか https://t.co/NRJVxAbJ1f
— Jaga Apple (@jagaapple_tech) April 6, 2020
つっつきボイス:「enumerizeはどこかで使ったことあった気がする🤔」「i18nに対応したenumということですね」「誰もが考えるヤツ😆: Railsでi18nに対応していない部分はちらほらあるので、そういう部分をこういうgemでサポートしたいでしょうね☺️」
「enumrizeを使うということは、要するにRails標準のenumを使わないということね: 自分もRails標準のenumは使いたくない派なのでワカル😆」「クエリビルダの汚染というのがまだイメージできてなくて😅」「自分もそこの意図はよくわかりませんが😆、Active Recordのenumを使うとシンボルとかをenum値に変換するので、ソースコードと実際のSQLを比較したときにわかりにくいコードになるのは確か😭」「やりにくそう...😅」
参考: ActiveRecordのenumで気をつけたい3つのポイント - Misoca開発者ブログ
「次のツイートも見つけました」「別テーブルと外部キー、個人的にはあんまりやらないかな: なおDBアドミニストレータ(DBA)の立場ならenumにする方がインデックスサイズは小さくなるから当然速くなるというのはありますね☺️」「ふむふむ」
enumの値がDBから見たら何なのかわからないって言うなら別テーブルにマスタ定義しておけばいいんじゃないかな…FK張っておけば存在しない値入らなくなるし… https://t.co/VQRlxJYE1D
— nazo (@nazo) April 8, 2020
「PostgreSQLならデータベースレベルでenum型があるので、やるならそっちを使う方がいいかなとちょっと思います😋」
- PostgreSQLドキュメント: 8.7. 列挙型
参考: 【Rails】5ステップでイケてる enum を作る(翻訳) - Qiita
⚓RailsがmacOSだと最近遅いのはなぜ?(Ruby Weeklyより)
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というサブドメインがあるとは」「見た感じそんなにアクティブという感じではなさそうですけど😆」「初歩的な質問にピンポイントでレスが付いておしまいになってるのも多いみたいです」「まあ気軽に質問できそうでいいかも😋」
⚓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使ってます😋」
「コメント欄に『Scout APMも載せるべき』とありますね」「今は『Rails APM』で検索すればScout APMが即出てきますヨ☺️」
⚓optimism: Rails用リモートバリデーションgem(Ruby Weeklyより)
- リポジトリ: leastbad/optimism
- デモサイト: OptimismDemo
- API: Reference - Optimism
つっつきボイス:「デモサイトがそっけないですね😆」「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
前編は以上です。
バックナンバー(2020年度第2四半期)
週刊Railsウォッチ(20200407後編)RubyのTracePointでデバッグ、Rubyとモナド、Gitノウハウ集、リモートワークほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。