こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Concurrent::Map.new
をスレッドセーフにした
- PR: Make sure that concurrent map usage is thread-safe by mensfeld · Pull Request #46536 · rails/rails
PREFIXED_PARTIAL_NAMES
が見つからないと、同時に複数のスレッド上で実行された場合にキーが上書きされる場合がある。
参照:Concurrent::Hash
default initialization is not fully thread-safe · Issue #970 · ruby-concurrency/concurrent-ruby
動機/背景
これと同じ: Remove not needed thread-safe primitive by mensfeld · Pull Request #46534 · rails/rails
しかしプルリクを分けるようガイドラインで指示されているため、2つ目のプルリクを作成した。
詳細
このプルリクはキャッシュミスの扱い方を変更する。これによりスレッドセーフになり、実行中に上書きされないようになる。
追加情報
参照: Remove not needed thread-safe primitive by mensfeld · Pull Request #46534 · rails/rails
参照:Concurrent::Hash
default initialization is not fully thread-safe · Issue #970 · ruby-concurrency/concurrent-ruby
同PRより
つっつきボイス:「スレッドセーフでなかった書き方をスレッドセーフにしたらしい」「#46534と関連しているんですね」「このcompute_if_absent
って初めて見たかも↓」
# actionview/lib/action_view/renderer/abstract_renderer.rb#L32
module ObjectRendering # :nodoc:
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
- h[k] = Concurrent::Map.new
+ h.compute_if_absent(k) { Concurrent::Map.new }
end
参考: Method: Concurrent::Map#compute_if_absent
— Documentation for concurrent-ruby (1.1.10)
「compute_if_absent
は、キーが存在しない場合に処理を実行して新しい値を保存する、つまりキーがある場合は何もしないんですね: ここではConcurrent::Map.new
が不要な場合には実行しないようにしている」「メモリアロケーションなどを細かく追わないとなかなか気づかなさそうですね」
🔗 before_committed!
コールバックをレコードの直近のコピーに対して実行するよう修正
以下と同じだが、
before_committed!
コールバックが対象である点が異なる。
Should attemptcommitted!
/rolledback!
to all enrolled records in the transaction by kamipo · Pull Request #36190 · rails/rails従来は、コールバック実行時に同じレコードの初期のコピー(オブジェクトidは異なるがデータベース上の同じレコードを指す)が使われていたが、
touch_later
の情報を持っていなかった。
修正: When a parent model hasbefore/after_commit/rollback
hooks, changing child attributes withaccepts_nested_attributes_for
causes parent to not be touched · Issue #26726 · rails/rails
同PRより
つっつきボイス:「committed!
とrolledback!
でも同じ修正が行われていたらしい↓」「この#36190は2019年の修正だから結構前ですね」
現在の
committed!
とrolledback!
は、トランザクション内で最初に登録(enroll)されたレコードだけを対象に試行するため、一部の振る舞いで問題が生じる。1つ目の問題は、トランザクションが確定していても、最初に登録されたレコード以外では
clear_transaction_record_state
が呼び出されないこと。
つまり、そのトランザクション内でde-duplicate(重複解消)されたレコードが最新のステートを参照しなくなる(例: レコードのステートがロールバックされなくなる)。2つ目の問題は、登録された順序が必ずしも実際に実行された登録順と同じとは限らないこと。最初に登録されたレコードは、何も実行せずに成功する可能性があり(例:
before_destroy
で別のレコードが既にdestroyに成功している)、その場合はトランザクション内でコールバックがトリガーされなくなってしまう。この2つの問題を回避するために、
committed!
とrolledback!
はそのトランザクション内で登録されたすべてのレコードを対象に試行するべき。
#36190より
🔗 recipients
メソッドでX-Forwarded-To
のアドレスを取れるようになった
このプルリクは、
Mail::Message#recipients
を拡張してメール転送先のアドレスを含めるようにする。
Action Mailboxでよくあるパターンのひとつに、ユーザーやチケットの識別子を含むアドレスに送信されたメールを処理するというものがある。このパターンでは、メールが送信されたアドレスがアプリケーションで必要になるので、この識別子を取得してモデル内を探索することになる。あるアドレスにメールが直接送信されると、
#recipients
はそのアドレスをTo
ヘッダーで検索する。しかし、あるアドレスにメールが転送された場合は、
#recipients
でそのアドレスを検索できない可能性がある。検索できるかどうかは、メールサーバーがFrom
ヘッダーとTo
ヘッダーをどう設定するかに依存する。メールサーバーはオリジナルのFrom
ヘッダーとTo
ヘッダーを維持することもあるが、維持しないこともある。その場合、メール転送先のアドレスは#recipients
から見えなくなってしまう。特にGmailがメールを転送する場合は、
From
ヘッダーとTo
ヘッダーを維持し、転送先アドレスをX-Forwarded-To
ヘッダーに記録する。従来は、メールサーバーが
To
ヘッダーに転送先アドレスを設定する場合はオリジナルのTo
を取得するサポートが追加されていた。このプルリクは、メールサーバーが
To
ヘッダーにオリジナルのTo
を設定する場合に転送先アドレスを取得するためのサポートを追加する。
同PRより
つっつきボイス:「Action Mailbox(Railsでメール受信を取り扱う機能)の改修ですね」「X-Forwarded-To
って何だっけ」「X-
で始まるから拡張系のメールヘッダーですね」「X-Forwarded-For
(HTTPヘッダー)かと思ったら違った」「昔このあたりのヘッダーをいじった覚えがあるようなないような」
参考: Action Mailbox の基礎 - Railsガイド
「Gmailはメール転送時のX-Forwarded-To
とTo/Fromの扱いが特殊なようで、そういったメールをAction Mailboxが処理する際、recipients
にX-Fowarded-To
が含まれていないと期待した挙動にならないので修正したらしい」「なるほど」「Action Mailboxはメールサーバーを作っているようなものだから、MTAが実装すべきものを頑張って実装している感じですね」
参考: Gmail | 自分のメールアドレス宛に届いたメールを他のメールアドレスへ自動転送する
参考: メール転送エージェント - Wikipedia -- MTA
🔗 FormBuilderの#fields
と#fields_for
でモデルをハッシュライクに渡せるようになった
fields_for
はパラメータを賢く扱おうとするが、名前と値が両方提供され、かつその値がオプションハッシュ形式の場合に混乱することがあった。この曖昧さを解消するために、空の場合であってもオプションHashを明示的に渡すようにする。<%= form_for @person do |person_form| %> ... <%= fields_for :permission, @person.permission, {} do |permission_fields| %> Admin?: <%= check_box_tag permission_fields.field_name(:admin), @person.permission[:admin] %> <% end %> ... <% end %>
同APIドキュメントより
動機/背景
ActionView::Helpers::FormBuilder#fields
にモデルを渡すとき、このモデルがHashまたはHashのサブクラスのインスタンス(extractable_options?
がtrueを返す)の場合(HashWithIndifferentAccess
など)、このモデルがオプションハッシュであるかのようにコードで解釈され、指定したモデルとオプションが両方とも無視されてしまう。問題は、パラメータを処理する
ActionView::Helpers::FormBuilder#fields_for
にある。#1778ではこの問題が部分的に修正されている:record_object
がHashのサブクラスで、extractable_options?
をオーバーロードしない場合は、Hash#extractable_options?
の制約のおかげですべて問題なく動く。しかし、
record_object
がHashそのものかHashWithIndifferentAccess
などの場合は引き続き問題がある(#46154)。この修正方法を思いつけなかったので、このプルリクには修正は含まれていない。しかし、呼び出し元コードが
fields_options
を提供しているかどうかを調べる形で、fields_for
に渡されるrecord_object
を強制的にチェックする方法を提供する。これによりfields
の問題が修正される(fields
メソッドの定義で、少なくともfields_options
に空ハッシュが渡されるようになるため)。詳細
このプルリクは、ActionView::Helpers::FormBuilder#fields_for
に提供されるパラメータの解釈方法を変更する。
record_object
パラメータが実際にはfields_options
パラメータであることが決定される前の段階で、fields_options
が明示的に渡されていないかどうかをチェックするようになった。
同PRより
つっつきボイス:「このfields_options
オプションにHashやHashの継承クラス(HashWithIndifferentAccess
など)の形でモデルを渡すと思わぬ挙動になったのを修正したということのようですね↓」「バグ修正に近い感じですね」
# actionview/lib/action_view/helpers/form_helper.rb#2267
- def fields_for(record_name, record_object = nil, fields_options = {}, &block)
- fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
+ def fields_for(record_name, record_object = nil, fields_options = nil, &block)
+ fields_options, record_object = record_object, nil if fields_options.nil? && record_object.is_a?(Hash) && record_object.extractable_options?
+ fields_options ||= {}
fields_options[:builder] ||= options[:builder]
fields_options[:namespace] = options[:namespace]
fields_options[:parent_builder] = self
参考: Rails API fields_for
-- ActionView::Helpers::FormBuilder
参考: Rails API fields
-- ActionView::Helpers::FormBuilder
「fields_for
というとaccepts_nested_attributes_for
で使う印象がありますね」「そもそもfields_for
をあまり使ってないかも」
参考: Rails API accepts_nested_attributes_for
-- ActiveRecord::NestedAttributes::ClassMethods
🔗 build
メソッドにハッシュの配列を渡すことで複数のオブジェクトをまとめてbuild
できるようになった
概要
new
のラッパーを提供することで、関連付けのbuild
メソッドと同様の記法を用いて、ハッシュを要素に持つ配列から複数のレコードをcreate
する機能と同等の機能を提供する。
- 関連付けは、ハッシュを要素に持つ配列から複数のオブジェクトを
create
できる- 関連付けは、ハッシュを要素に持つ配列から複数のオブジェクトを
build
できる- クラスは、ハッシュを要素に持つ配列から複数のオブジェクトを
create
できる- クラスは、ハッシュを要素に持つ配列から複数のオブジェクトを
build
できる(←今ここ)こちらで議論された: Allow ActiveRecord::Inheritance#new to accept an array - rubyonrails-core - Ruby on Rails Discussions
同PRより
つっつきボイス:「テストコードを見ると、Topic.build([{ title: "first" }, { title: "second" }])
みたいにbuild
にハッシュを要素に持つ配列をクラスにどさっと渡せるようになっていますね↓」「お〜ありがたそう」「どれか1つが失敗するとまとめて失敗するのかな?」「これができるとテストコードで便利そう👍」
# activerecord/test/cases/persistence_test.rb#L429
def test_build
topic = Topic.build(title: "New Topic")
assert_equal "New Topic", topic.title
assert_not_predicate topic, :persisted?
end
def test_build_many
topics = Topic.build([{ title: "first" }, { title: "second" }])
assert_equal ["first", "second"], topics.map(&:title)
topics.each { |topic| assert_not_predicate topic, :persisted? }
end
def test_build_through_factory_with_block
topic = Topic.build("title" => "New Topic") do |t|
t.author_name = "David"
end
assert_equal("New Topic", topic.title)
assert_equal("David", topic.author_name)
assert_not_predicate topic, :persisted?
end
def test_build_many_through_factory_with_block
topics = Topic.build([{ "title" => "first" }, { "title" => "second" }]) do |t|
t.author_name = "David"
end
assert_equal 2, topics.size
topics.each { |topic| assert_not_predicate topic, :persisted? }
topic1, topic2 = topics
assert_equal "first", topic1.title
assert_equal "David", topic1.author_name
assert_equal "second", topic2.title
assert_equal "David", topic2.author_name
end
# activerecord/lib/active_record/persistence.rb#83
def build(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr, &block) }
else
new(attributes, &block)
end
end
🔗Rails
🔗 Hotwire devニュースレター
I'm not using it yet because of these reasons. :)
But it will appear on https://t.co/MVN2FFofm6. It's a static site powered by Jekyll.
— Joe Masilotti 📗 (@joemasilotti) December 1, 2022
つっつきボイス:「Hotwireの月刊ニュースレターです」「月イチぐらいだと気軽に読めそう👍」「Railsウォッチ的なノリで記事や情報を紹介している感じ」「親しみを感じちゃいました😋」
「September 2022 editionを見ていたらRails Hackersonというイベントが紹介されていました↓」「Winnersが出ているということはもう終わってるのか〜」「48時間以内に作るルールなんですね」「昔はよくこういう週末に手早く作りきるみたいなハッカソンイベントがよく開催されていましたが、最近減った気がしますね: Railsは元々短時間でモノを作ってリリースするのに向いているので面白そう」「たしかに」
参考: Rails Hackathon
参考: Rails Hackathon 2022 Winners
参考: ハッカソン - Wikipedia
「ところで以下のツイートをたまたま見かけたんですが、Honoという軽量JSフレームワークを作っている方だそうです」「Honoって炎のことですか🔥」「RailsやRubyを使っていない人にもHotwireがアピールしているんですね」
Hotwireめっちゃいい。まじでいい。
— Yusuke Wada (@yusukebe) November 28, 2022
参考: Hono - Ultrafast web framework for Cloudflare Workers, Deno, and Bun.
🔗 Rubyのコンカレンシーは難しい: いかにして私がRailsコントリビュータになったか(Ruby Weeklyより)
つっつきボイス:「karafkaを使っているんですね」「karafkaはApacheの分散ストーリーミング基盤であるKafkaをRubyで使えるようにするgemでしたね」
参考: Apache Kafka
参考: Apache Kafka とは?動作確認や機能、特徴などを解説 | OSSサポートのOpenStandia™【NRI】
「記事は、Karafkaで踏んだコンカレンシーの問題を調査しているうちに、以下のような書き方だとブロックのロックが不完全になることを見つけたらしい↓」「うわ〜」
# 同記事より
semaphores = Concurrent::Hash.new { |h, k| h[k] = Queue.new }
# 以下は上と同等
semaphores = Concurrent::Hash.new do |h, k|
queue = Queue.new
h[k] = queue
end
「こういうスレッドのデバッグはつらそう↓」「考えたくない...」「踏みたくない...」「コンカレンシーをスレッド内で解決しようとしてミスるとこのあたりの問題を踏みがちで、しかも問題がメモリ内で起きるからデバッグが大変」「だからジョブキューを使ったソリューションにする人が多いんだろうな」「ジョブキューならログですぐ調べられますしね」
「この問題はたとえば以下のようにして修正できるらしい↓」「Concurrent::Hash
をConcurrent::Map
に置き換えて#compute_if_absent
を使うか、Concurrent::Hash
の初期化ブロックにロックを導入することで解決できると書かれていますね」「ここでもさっきの#compute_if_absent
が登場している」「RubyでMutex#synchronize
を書いたことなかった」
# 同記事より
Concurrent::Map.new do |k, v|
k.compute_if_absent(v) { [] }
end
mutex = Mutex.new
Concurrent::Hash.new do |k, v|
mutex.synchronize do
break k[v] if k.key?(v)
k[v] = []
end
end
参考: Thread::Mutex#synchronize
(Ruby 3.1 リファレンスマニュアル)
参考: ミューテックス - Wikipedia
「記事では、これと同じような問題がRailsやいろんなgemにも潜んでいたことをSourcegraphで発見したんですって」「記事後半でRailsやgemにいろいろプルリクを投げていますね」「すごい」「タイトルにあったRailsへの貢献はそういうことだったのか」
参考: Sourcegraph
(問題が見つかったプロジェクト)
- rails (activesupport and actionview)
- i18n
- dry-schema
- finite_machine
- graphql-ruby
- rom-factory
- apache whimsy
- krane
- puppet
同記事より
「記事の著者がRailsに投げた修正プルリク↓を見たら、さっきつっついた#46536でした」「初コントリビュートがスレッドバグの修正ってなかなかなさそうですよね」「Railsのように大勢の目が光っていてもこういう問題が残っていたぐらいだから、やっぱりスレッドは難しい」「難しいです」
参考: Make sure that concurrent map usage is thread-safe by mensfeld · Pull Request #46536 · rails/rails
参考: Remove not needed thread-safe primitive by mensfeld · Pull Request #46534 · rails/rails
🔗 pessimize gemでgemを安全にアップグレードする(RubyFlowより)
つっつきボイス:「Hanami公式ブログの記事です」「今使われている詳細なGemバージョンを最低バージョンとして指定したpessimistic operator(~>
)を現在のGemfileに自動で付けてくれるようですね」「割と前からあるgemっぽい」
# 同記事より
source "<https://rubygems.org>"
gem "puma", "~> 5.6"
gem "roda", "~> 3.61"
gem "awesome_print", "~> 1.9"
gem "pg", "~> 1.4"
gem "redis", "~> 5.0"
gem "money", "~> 3.0"
gem "shrine", "~> 3.4"
gem "cloudinary", "~> 1.23"
gem "shrine-cloudinary", "~> 1.1"
gem "faker", "~> 2.23"
gem "dry-struct", "~> 1.4"
gem "dry-validation", "~> 1.8"
gem "dry-schema", "~> 1.10"
「pessimistic operatorを指定するとbundle update
したときに最終桁のバージョンだけ上げていくようになる(参考リンク↓)ので、一般的なバージョンの最終桁をpatch versionとしてメンテされているGemであれば、pessimizeを使って安全にbundle update
できるGemfileになるということですね」「なるほど」
参考: Ruby's Pessimistic Operator
「gemをエイヤでアップグレードするより安全にやれそう」「アップグレード時のバージョンが気になる人は使ってみてもよさそう👍」
「ところで、gemのバージョンをあまり細かく指定するのは考えものですね: 他のgemと相互依存している場合にエラーになったこともあります」「そうそう」「pessimizeの場合は基本的に~>
になるようなので多少安心だけど、気をつけたい」
🔗 その他Rails
#kaigionrails 2022 のデザインをお手伝いさせていただいた話(を書きました)
🎨 Kaigi on Rails 2022のデザインお手伝い || https://t.co/cHpA2Ibnl5https://t.co/Kw8NTGmOwk
— ksm.equip(mask + (💉*4)) (@ksmxxxxxx) November 25, 2022
つっつきボイス:「Kaigi on Rails 2022のデザイナーさんによる記事です」「こういう情報も共有されるのは嬉しいですね」「ノベルティグッズのデザインも手がけているのね」「プロのデザイナーが入ると歴然とクォリティに差が出ますね👍」
前編は以上です。
バックナンバー(2022年度第4四半期)
- 20221129前編 Hanami 2.0リリース、Railsに関わる技術の体系化を目指した本ほか
- 20221122 The Rails Foundation発足、Ruby 3.2.0 Preview 3リリース、Ruby演算子クイズほか
- 20221116後編 Rubyを使っている企業の時価総額リスト、irbのshow_source、GitHub Codespacesほか
- 20221115前編 RailsチュートリアルがRails 7対応版をリリース、ViewComponentで使えるLookbookほか
- 20221102後編 書籍『Programming Ruby 3.2 (5th Edition)』、ReDoSチェックサイトほか
- 20221101前編 Packwerkの詳しい解説書『Gradual Modularization for Ruby and Rails』ほか
- 20221026後編 Ruby 3.2のData.define、RubyPrize 2022最終ノミネート、Puma-dev gemほか
- 20221025前編 rodauth-rails gem作者の解説記事、turbo-railsの有料チュートリアルほか
- 20221019後編 Ruby技術者認定試験再受験無料キャンペーン、Starlink日本で販売開始ほか
- 20221018前編 Rails向けLanguage Server “refreshing”開発中、JetBrains Fleetほか
- 20221012後編 RailsとPostgreSQLで列挙型を作成する6つの方法、Ubuntu Proほか
- 20221011前編 Turbo 7.2.0リリース、GitLabのDevSecOpsサーベイ結果ほか
- 20221004後編 ヒアドキュメント拡張の提案、『組織に自動テストを根付かせる戦略』ほか
- 20221003前編 Kaigi on Rails 2022のタイムテーブル発表、書籍『Practicing Rails』ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)