- Ruby / Rails関連
週刊Railsウォッチ: solid_queueとmission_control-jobsが正式にRailsのgemに、Rubyの"チルド"文字列ほか(20240402)
こんにちは、hachi8833です。xzの脆弱性対策をお忘れなく。
“XZ Utilsの脆弱性 CVE-2024-3094 についてまとめてみた - piyolog” https://t.co/QYpX58DweW
— 徳丸 浩 (@ockeghem) April 1, 2024
sshの0.5秒の遅延からxzのバックドアを発見し大惨事を未然に防いだAndres Freundの功績を讃えるMicrosoft CEO https://t.co/FwVEVIZg6p
— Haruhiko Okumura (@h_okumura) April 1, 2024
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Active Storageでプリプロセス不要なファイルをエンキューしないよう修正
#51030のフォローアップとして、「バリアントを一切指定していない」または「
TransformJob
でプリプロセスが必要なバリアントを含まない」プレビュー可能なattachedファイルの振る舞いを変更する。従来は、プリプロセスが必要なバリアントがない場合、Attachment#transform_variants_later
は何もしなかった(named_variants
が以下のように空になる)。def transform_variants_later named_variants.each do |_name, named_variant| blob.preprocessed(named_variant.transformations) if named_variant.preprocessed?(record) end end
#51030以後は、attachedファイルのblobと空配列を持つ
ActiveStorage::PreviewImageJob
がエンキューされるようになり、以下のようにblob.create_preview_image_later(preprocessed_variations)
の呼び出しでpreprocessed_variations
が空になっていた。def transform_variants_later preprocessed_variations = named_variants.filter_map { |_name, named_variant| if named_variant.preprocessed?(record) named_variant.transformations end } if blob.preview_image_needed_before_processing_variants? blob.create_preview_image_later(preprocessed_variations) else preprocessed_variations.each do |transformations| blob.preprocessed(transformations) end end end
自分たちの場合、予想していなかった新しいジョブが大量に流れ込み、さらに受信メール処理中に作成されたblobとattachedファイルがデッドロックしたためにジョブが大量に失敗し、非常に驚かされた。
そのため、インラインプレビューを作成するタイミングは、これらのblobが取り込まれてAction Textのリッチテキストレコードに埋め込まれるときではなく、プレビューが要求された時点で意図的に行うようにした。
blobを取り込んでからInboundEmail::RoutingJob
の処理と並行してプレビューを作成すると、2つのジョブで同時にtouch
がトリガーされて階層を駆け上がり、デッドロックが発生する可能性が非常に高くなる。
同PRより
つっつきボイス:「Active Storageではattachedファイルのバリアント(サイズ違いの画像)生成をジョブキューに送って生成できるけど、バリアントの生成が不要なファイルまでジョブが生成されていたのか」「そのせいで大量にジョブが発生してデッドロックでびっくりしたとありますね」「修正は&& preprocessed_variations.any?
を追加することで行ったのね↓: 何も処理しないとわかっているジョブは、正常に終了できるとしてもエンキューすべきでないと自分も思います👍」
# activestorage/app/models/active_storage/attachment.rb#L134
def transform_variants_later
preprocessed_variations = named_variants.filter_map { |_name, named_variant|
if named_variant.preprocessed?(record)
named_variant.transformations
end
}
- if blob.preview_image_needed_before_processing_variants?
+ if blob.preview_image_needed_before_processing_variants? && preprocessed_variations.any?
blob.create_preview_image_later(preprocessed_variations)
else
preprocessed_variations.each do |transformations|
blob.preprocessed(transformations)
end
end
end
参考: Active Storage の概要 - Railsガイド
🔗 Active Record内に残っているlease_connection
を排除
Ref: #51349
通常のActive Record APIが使われるときに恒久的なリースを取得できないようにする。排除した恒久的リースのいくつかは理想的な形で削除できなかったので、いくつかTODOやFIXMEを追加した。将来改善されると願っているが、これらを修正して
config.active_record.permanent_connection_checkout
を7.2で実験できるようにしたかった。
同PRより
つっつきボイス:「byrootさん(casperisfineは別名)がこの間から取り組んでいるActiveRecord::Base.with_connection
周りの改修の続きのようですね(ウォッチ20240312)(ウォッチ20240228)」「あのときBase.connection
は非推奨化処理はされずに今後も使えることになりましたけど、このプルリクを見た感じでは、その後#51349がマージされてconfig.active_record.permanent_connection_checkout
というコンフィグが7.2で追加されることになって、非推奨警告を出したければ出せるようにしたんですね: このコンフィグの動作を確かめる準備として、残っていたlease_connection
をまず削除したという流れのようですね」「なるほど」
# activerecord/lib/active_record/associations/join_dependency/join_association.rb#L92
private
- def append_constraints(join, constraints)
+ def append_constraints(connection, join, constraints)
if join.is_a?(Arel::Nodes::StringJoin)
join_string = Arel::Nodes::And.new(constraints.unshift join.left)
- join.left = Arel.sql(base_klass.lease_connection.visitor.compile(join_string))
+ join.left = Arel.sql(connection.visitor.compile(join_string))
else
right = join.right
right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
end
end
🔗 Railsエンジンが多くなると起動が遅くなる問題を修正
#47347を部分的にやり直した。
現在、すべての
Rails::Engine
を初期化すると、エンジンがパスを先頭に追加するときに新しいViewウォッチャーが作成される。ActiveSupport.on_load(:action_controller) { prepend_view_path(views) if respond_to?(:prepend_view_path) }
これは、遅延読み込みされたアプリケーションで最初のコールドリクエストを実行するのに要する時間に影響する。
prepend_view_path
->_build_view_paths
->cast_file_system_resolvers
->file_system_resolver_hooks.each(&:call)
->rebuild_watcher
この変更により、最初の
updated?
チェックが実行されるまでViewウォッチャーの初期化が遅延され、ウォッチャーの初期化が1回だけで済むようになる。ベンチマーク
このベンチマークは完全ではないいものの、不要なウォッチャーのリビルドの振る舞いや影響を確実に示していると思う。
(ベンチマークコードは省略)
- mainブランチ: Time: 17.98 ms
- このブランチ: Time: 2.93 ms
自分たちのモジュール式モノリスでは、ビューパスを冒頭に追加するエンジンが100個ほどあり、ウォッチャーをリビルドするディレクトリリストが増大していた。この変更により、コールドリクエストが最大16秒から最大8秒まで短縮される。
同PRより
つっつきボイス:「Viewウォッチャーって何だろうと思ったら、Action Viewの内部で使われているViewReloader
クラスのウォッチャー機能のことみたいですね」「Rails::Engine
を初期化するたびにウォッチャーが作成されるせいで、Railsエンジンが多いと起動が遅くなっていたので、初期化を遅延評価することで修正したということか」「エンジンが100個ほどあるRailsアプリってすごいですね」「想像だけどマイクロサービス的にエンジンに分けて実装しているのかもしれませんね」
🔗 複合主キー関連の修正: 関連付けのprimary_key
オプションに配列を渡せるようになった
修正: #50850
#49671のコメントも少し関連している。関連付けの
primary_key
は、関連付けられるクラスのprimary_key
やquery_constraints
から導出された場合は複合可能だが、Railsが既にサポート可能であっても、明示的に設定することは許されていない。このコミットによって、関連付けの
primary_key
オプションに配列を渡せるようになる。以下の例では、
primary_key
が既に配列にできるようになっていることが前提。if custom_primary_key.is_a?(Array)
同PRより
つっつきボイス:「お、複合主キー関連の改修は落ち着いたかと思ったらまだあった: モデルのbelongs_to
で、このテストコード↓のprimary_key: [:shop_id, :id]
のようにprimary_key
オプションに配列を渡せるようになったことで複合主キーが使えるようになったんですね👍」
# activerecord/test/models/cpk/book.rb#L3
module Cpk
class Book < ActiveRecord::Base
attr_accessor :fail_destroy
self.table_name = :cpk_books
belongs_to :order, autosave: true, query_constraints: [:shop_id, :order_id], counter_cache: true
_ belongs_to :order_explicit_fk_pk, class_name: "Cpk::Order", query_constraints: [:shop_id, :order_id], primary_key: [:shop_id, :id]
belongs_to :author, class_name: "Cpk::Author"
has_many :chapters, query_constraints: [:author_id, :book_id]
before_destroy :prevent_destroy_if_set
private
def prevent_destroy_if_set
throw(:abort) if fail_destroy
end
end
# activerecord/lib/active_record/reflection.rb#L871
def association_primary_key(klass = nil)
if primary_key = options[:primary_key]
- @association_primary_key ||= -primary_key.to_s
+ @association_primary_key ||= if primary_key.is_a?(Array)
+ primary_key.map { |pk| pk.to_s.freeze }.freeze
+ else
+ -primary_key.to_s
+ end
elsif (klass || self.klass).has_query_constraints? || options[:query_constraints]
(klass || self.klass).composite_query_constraints_list
elsif (klass || self.klass).composite_primary_key?
# If klass has composite primary key of shape [:<tenant_key>, :id], infer primary_key as :id
primary_key = (klass || self.klass).primary_key
primary_key.include?("id") ? "id" : primary_key
else
primary_key(klass || self.klass)
end
end
🔗Rails
🔗 solid_queueとmission_control-jobsがRailsのリポジトリに追加された(Rails公式ニュースより)
つっつきボイス「あれ、mission_control-jobsはActive Job用に37signalsが公開したジョブダッシュボードだったけど、この間取り上げていましたよね?(ウォッチ20240227)」「あ、たしかに」「よく見たら、以前mission_control-jobsを取り上げたときは37signalsのGitHubリポジトリ(リポジトリ名は旧社名のbasecamp)に置かれていたのか: mission_control-jobsがRailsのリポジトリに正式に取り入れられたことで今回Rails公式ニュースでアナウンスされたんですね」「なるほど、そういう流れだったんですね」
Same too for the Mission Control UI for managing Active Job queues: https://t.co/FZZZrM1z9i
— DHH (@dhh) March 20, 2024
「そしてSolid Queueも最初に取り上げたとき(ウォッチ20240117)は37signalsのリポジトリにあったけど、これも今見るとRailsのリポジトリにリダイレクトされました」
🔗 論理削除でカオスと化した日(Ruby Weeklyより)
つっつきボイス:「記事を書いた人は論理削除で痛い目に遭ったようです」「論理削除やソフトデリートと言われているものは、Railsとかがなかったような昔から一般的なDB設計でいつも問題になるヤツですね: まさに歴史は繰り返す」
参考: 論理削除(ソフトデリート)とは - 意味をわかりやすく - IT用語辞典 e-Words
「ちなみに記事に登場しているacts_as_paranoidというgemは論理削除で使われていたものですが、今は古いのでparanoiaというgemに移行しています↓」「そういえばacts_as_で始まるgemは古いものが多いという話がありましたね(ウォッチ20220221)」「なるほど」「論理削除gemは他にもいくつかあった覚えがあります」
🔗 props_template: JbuilderとAPIが近い代替gem(Ruby Weeklyより)
つっつきボイス:「RailsでビューをJSONで出力するJbuilderの代替gemはいろいろありますが、これはthoughtbotが作ったものだそうです」「Rails標準のJbuilderは遅いので有名ですね」
参考: 3.3 Jbuilder -- Action View の概要 - Railsガイド
同リポジトリより
「props_templateのREADMEにもベンチマークのグラフが掲載されていますね↑」「それよりも、APIがJbuilderと非常に近いと書かれているのがポイントでしょうね、実際似ていますし↓」
# 同リポジトリより
json.flash flash.to_h
json.menu do
json.currentUser do
json.email current_user.email
json.avatar current_user.avatar
json.inbox current_user.messages.count
end
end
json.dashboard(defer: :auto) do
sleep 5
json.complexPostMetric 500
end
json.posts do
page_num = params[:page_num]
paged_posts = @posts.page(page_num).per(20)
json.list do
json.array! paged_posts, key: :id do |post|
json.id post.id
json.description post.description
json.commentsCount post.comments.count
json.editPath edit_post_path(post)
end
end
json.paginationPath posts_path
json.current pagedPosts.current_page
json.total @posts.count
end
json.footer partial: 'shared/footer' do
end
「jbuilderのようなJSON/XML出力定義用の代替gemは昔からいろいろありますけど、既にJbuilderを広範囲に利用しているRailsアプリでJbuilderを代替gemに差し替える場合、実装方法が異なっていると単純な検索置換ができなくて移行作業量が増えてしまうんですよ」「コードを読んで置き換えてテストしてとなるとつらそうですね」「まあJbuilderは遅いのがわかっているので最初から使わないようにしていますけどね」「そういえば自分のアプリでもrails new
の時点でJbuilderを無効にしました」
「props_templateは、"Jbuilder-like DSL"と書かれているので完全に同じではないのかもしれないけど、Jbuilderと書き方が非常に近いというのは乗り換え候補として検討する価値が上がるでしょうね👍」
🔗Ruby
🔗 "チルド"文字列がRubyに入る(Ruby Weeklyより)
- commit: Implement chilled strings · ruby/ruby@12be40a -- マージ済み
つっつきボイス:「Ruby WeeklyでRubyに"チルド"文字列というものがマージされたというニュースを見かけたので取り上げました」「frozenではないけどfrozenみたいな文字列だからチルドと呼んでいるのかな」「今後はチルド文字列を書き換えようとすると警告が表示されるみたいですね」
このコミットでは、将来
frozen_string_literal: true
をデフォルトでを有効にするためのパスとして「chilled string(以下、チルド文字列)」を導入する。チルド文字列は、ユーザーの視点から見ると文字列はfrozenのふりをするが、最初に文字列を変更しようとするとfrozenステータスを失い、FrozenError
を発生せずに警告を発する。
rb_compile_option_struct.frozen_string_literal
は、事実上もはやブール値ではなくなり、「enabled(有効)/disabled(無効)/unset(未設定)」の3つの状態を持つようになる。明示的にenableにもdisabledにも設定していないfrozen string literalを用いるコードがコンパイルされると、その文字列リテラルは新しい
putchilledstring
インストラクションを用いてコンパイルされる。このインストラクションは、文字列でSTR_CHILLED (FL_USER3)
フラグとFL_FREEZE
フラグをオンにすることを除いてputstring
と同じである。チルド文字列には
FL_FREEZE
フラグがあり、コードベース全体でチルド文字列をチェックする必要性を最小限に抑え、C拡張機能との互換性を向上させる。メモ:
String#freeze
: チルドフラグをクリアするString#-@
: 文字列はミュータブル(変更可能)であるかのように振る舞うString#+@
: 文字列はミュータブル(変更可能)であるかのように振る舞うString#clone
: チルドフラグをコピーする共著者: Jean Boussier byroot@ruby-lang.org
同コミットより
参考: String#-@
(Ruby 3.3 リファレンスマニュアル)
参考: String#+@
(Ruby 3.3 リファレンスマニュアル)
「Rubyがfrozen_string_literal: true
を導入して以来、いつかこれをデフォルトにすると言い続けていたけど、Rubyの場合は文字列のミューテーションが基本操作として広範囲で使われていることもあって、なかなか実現していないんですよ」「そういえばPythonは昔から文字列がイミュータブル(改変不可)ですけど、Rubyでそれを後から実現するのは大変そうですね」「frozen_string_literal: true
をいきなりデフォルトにするのは影響が大きいので、段階的に進める施策のひとつとしてこのチルド文字列を導入したんじゃないかなと思います」
なぜ Python の文字列はイミュータブルなのですか?
これにはいくつかの利点があります。
一つはパフォーマンスです。文字列がイミュータブルなら、生成時に領域を割り当てることができるので、必要な記憶域は固定されて、変更されません。これはタプルとリストを区別する理由の一つでもあります。
他の利点は、Python の文字列は数と同じくらい "基本的" なものと考えられることです。8 という値を他の何かに変える手段が無いように、文字列 "eight" を他の何かに変える手段も無いのです。
デザインと歴史 FAQ — Python 3.6.15 ドキュメントより
以下は直接関係ありませんが、つっつき後に見つけたjnchitoさんの昔のツイートです。
<Ruby豆知識>
# frozen_string_literal: true は全ての文字列がデフォルトで凍結されるわけではなく、文字列リテラルを使って生成した文字列だけが凍結されます。なので、数値や配列をto_sやjoinメソッド等で文字列化した場合は、この条件に該当しないので凍結されません。https://t.co/KH87pdJNou pic.twitter.com/ABAFdm5qTB— Junichi Ito (伊藤淳一) (@jnchito) August 2, 2022
🔗 TruffleRuby 24.0.0はRuby 3.2/3.3構文を完全サポート(Ruby Weeklyより)
つっつきボイス:「お、TruffleRubyが新しくリリースされて、しかも例のPrismパーサーの採用でRuby 3.2/3.3構文に完全対応した🎉」「TruffleRubyのチームもPrism開発に協力していた甲斐あって、Prismが本領発揮してきた感じですね」
🔗 Time#utc
は破壊的メソッド(Ruby Weeklyより)
✨ RUBY PRO TIPS ✨
Did you know that Time#utc modifies the receiver? 🤔
Instead, use Time#getutc ✨💫#rubyonrails pic.twitter.com/WsK1IaMR2m
— RubyCademy (@RubyCademy) March 14, 2024
つっつきボイス:「これは知らなかった: Time#utc
はそのTimeオブジェクトをUTCに変換した新しいオブジェクトを返すように見えて、実はそのTimeオブジェクト自体を破壊的にUTCに変える」「言われてみればメソッド名というより属性の読み出しっぽく見えそうですね」「このメソッド名何とかしたい」
参考: Time#gmtime
(Ruby 3.3 リファレンスマニュアル) -- Time#utc
はエイリアス
🔗言語/ツール/OS/CPU
🔗 GitHub、Copilot使いこなしのコツをGitHubが公式に公開
つっつきボイス:「Copilotを使うときは関連ファイルをできるだけ開いておくと効果的という話は以前もおっしゃっていましたね」「既に知られているコツもいくつか含まれていますし、意味のある命名はCopilotがなくても重要ですけど、そういう情報を公式にGitHubが出してくれたのがいいですね👍」「サンプルコード、やっぱり効くんですね」
🔗 RFCを公開しているサイト
つっつきボイス:「RFCを引用するときの望ましい方法なども調べてあって今回もいい記事👍」
「言われてみればRFCを検索して出てくるサイトはいくつもありますね」「最近になって増えているとは知りませんでした」「どれを参照するのが望ましいかをIETFのdiscussionで聞いてみたのすごい」
今回は以上です。
バックナンバー(2024年度第1四半期)
週刊Railsウォッチ: Rails用のHTTP/2プロキシThrusterが登場、Rails Guidesが内容とスタイルを一新へほか(20240328後編)
- 20240326前編 Active StorageでIllustratorファイルをMuPDFとPopplerでプレビュー可能にほか
- 20240313後編 Rubyでシリアルポートにアクセス、Active Record vs Sequelほか
- 20240312前編 Rails 8に入るSolid Cacheほか
- 20240228 Rails 8でSprocketsがPropshaftに置き換わる、devcontainerサポートほか
- 20240227後編 Turbo Nativeアプリ、書籍『Everyday Rails Testing with RSpec』新版執筆開始ほか
- 20240221前編 form_withのmodelオプションへのnil渡しが非推奨化、Dockerfileでjemallocが有効にほか
- 20240207後編 aws-sdk-rubyの全gemにRBSファイルが追加ほか
- 20240206前編 Pumaのデフォルトスレッド数変更、Rails 1.0をRuby 3.3で動かすほ
- 20240125後編 RailsコントローラのparamsはHashではない、ruby-enumほか
- 20240123前編 Railsの必須Rubyバージョンが3.1.0以上に変更ほか
- 20240119後編 Ruby 3.3でYJITを有効にすべき理由、Turbo 8の注意点8つほか
- 20240117前編 Rails 8マイルストーン、2023年のRails振り返り、Solid Queueほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)