- Ruby / Rails関連
週刊Railsウォッチ: Rails公式のdevcontainerでKamalをサポート、RailsBump.orgほか(20241108)
こんにちは、hachi8833です。Rails 8.0.0が思ったより早くリリースされました🎉
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 config.active_job.enqueue_after_transaction_commit
の従来の設定方法が非推奨化
設定方法も従来のシンボルからブーリアンに変更する。
この設定値はキューアダプタに委譲されなくなった(この設定で制御される振る舞いは、ユーザーが制御できない振る舞いの変更をそのままにしておくと危険すぎるため)。
修正 #52675。
同PRより
つっつきボイス:「enqueue_after_transaction_commit
そのものが非推奨になったわけではなくて、これまで:never
、:always
、:default
のいずれかを設定可能だったのをブーリアンのみ設定可能に変更するということのようですね」「コンフィグガイドからはenqueue_after_transaction_commit
の項目が削除されていました」
参考: 3.15.5 config.active_job.enqueue_after_transaction_commit
-- Rails アプリケーションの設定項目 - Railsガイド
🔗 Rails公式のdevcontainer内でもKamalをサポート
devcontainerにKamalのサポートを追加
従来は、生成したdevcontainerでDockerを利用できなかったため、Kamalを利用できなかった。
Joé Dupuis
同Changelogより
Kamalを実行するにはDockerが必要。devcontainerのdocker-in-docker機能を使うと、コンテナー内で Dockerを実行可能になる。
VS CodeのDocker credentialsの問題
VS Codeのdevcontainerでは、Dockerを非rootユーザーとして実行できないという問題がある。VS Codeはコンテナー内でDocker credentialの転送を試みるが、これにより非rootユーザーとしてDockerを使えなくなる。
これを解決するには、VS Codeのdevcontainer拡張機能で
Docker Credential Helper
を無効にする必要がある。関連issue:
自分がこのことをRailsガイドのdevcontainerセクションに追記してもよい。
docker-in-docker
とdocker-outside-docker
あるいは、docker-in-docker (dockerの中にdockerの中にdocker...)機能の代わりにdocker-outside-docker機能を使うことも可能。これにより、ホストDockerソケットがコンテナーに公開され、パフォーマンスは向上するが、セキュリティが犠牲になる。
ここではパフォーマンスを優先するのが合理的と思われるが、この機能はDocker経験の浅い初心者が使う可能性が高いため、セキュリティを優先するのが妥当と思われる。
特に強い意見があるわけではないので、
docker-outside-docker
機能に切り替えても構わない。同PRより
つっつきボイス:「docker-in-dockerとdocker-outside-of-dockerという機能がDockerにあるんですね」「devcontainerはそもそもDockerで動きますし、例のKamalはデプロイ用のコンテナを作ってデプロイするツールなので、ざっと見た感じでは、devcontainerのコンテナからKamalのコンテナを作成する必要があるということかな」
「プルリクメッセージによると、docker-outside-of-dockerはパフォーマンスはいいけどセキュリティが劣る、docker-in-dockerはセキュリティはいいけどパフォーマンスが劣るので、どっちにしようか悩んでるみたいですね」「以下の記事にわかりやすい図がある↓」
参考: dind(docker-in-docker)とdood(docker-outside-of-docker)でコンテナを料理する #Docker - Qiita
(docker-outside-of-docker)
(docker-in-docker)
dind(docker-in-docker)とdood(docker-outside-of-docker)でコンテナを料理する #Docker - Qiitaより
「devcontainersのリポジトリにあるdocker-in-dockerの設定を見ると、/var/lib/docker
をマウントする方法でやっていますね↓: 上の図のように、ホストマシンとは完全に独立したDocker環境をDockerの中に構築できるらしい」「なるほど」
参考: features/src/docker-in-docker at main · devcontainers/features
// https://github.com/devcontainers/features/blob/main/src/docker-in-docker/devcontainer-feature.json#L77
"mounts": [
{
"source": "dind-var-lib-docker-${devcontainerId}",
"target": "/var/lib/docker",
"type": "volume"
}
],
「同じくdocker-outside-of-dockerの設定ではsocketファイルをホストと共有するようになっている↓: たぶんこうすると、このDockerコンテナ内でdocker
コマンドを使えば"ホスト側の"Dockerを参照できる」「なるほど」「その気になれば自分で自分のコンテナを終了させることも可能でしょうね😆」
参考: features/src/docker-outside-of-docker at main · devcontainers/features
// https://github.com/devcontainers/features/blob/main/src/docker-outside-of-docker/devcontainer-feature.json#L57-L63
"mounts": [
{
"source": "/var/run/docker.sock",
"target": "/var/run/docker-host.sock",
"type": "bind"
}
],
「docker-in-dockerは昔からあるわかりやすいセットアップかな」「rafaelのコメントを見ると、development環境でしか使わない機能なのでセキュリティよりパフォーマンスが重要、ならばdocker-outside-of-dockerにしようという結論になってますね」
「このあたりはもう少し詳しく見てみないとわからないけど、Kamalはローカルからデプロイするツールなのでdocker-outside-of-dockerにしたのもわかりますけど、デプロイツールとしての権限が結構強いので、docker-in-dockerの方がいいのではという気もしますね: たとえばCIとかデプロイ専用サーバーとかでdocker-outside-of-dockerにしたとすると、アクセスしてはいけない隣のDockerコンテナにアクセスしてしまうでしょうし」「う〜む」「docker-in-docker周りはそのうち調べてみようかな」
🔗 enum
のラベルをキーワード引数としても渡せるようになった
ラベルをキーワード引数としてenumに渡せることについては、#41328でも意図されていたことなので、それをドキュメント化してテストでカバーした。
# rails/activerecord/lib/active_record/enum.rb Lines 60 to 65 in 0618d2d # Finally, it's also possible to explicitly map the relation between attribute and # database integer with a hash: # # class Conversation < ActiveRecord::Base # enum :status, active: 0, archived: 1 # en
# rails/activerecord/test/cases/enum_test.rb Lines 702 to 713 in 0618d2d test "option names can be used as label" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" enum :status, default: 0, scopes: 1, prefix: 2, suffix: 3 end book = klass.new assert_predicate book, :default? assert_not_predicate book, :scopes? assert_not_predicate book, :prefix? assert_not_predicate book, :suffix? end
コードベースを簡素化するためにサポートを削除することについては反対するつもりはないが、少なくとも削除する前に非推奨化する必要がある。
修正: #53407
同PRより
つっつきボイス:「これと次のプルリクはkamipoさんによるものです」「元々ラベルをキーワード引数としてenumに渡せるようになっていたのを、インターフェイスを整えてテストも足したんですね👍」
# activerecord/lib/active_record/enum.rb#L216
- def enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
- assert_valid_enum_definition_values(values)
- assert_valid_enum_options(options)
+ def enum(name, values = nil, **options)
+ values, options = options, {} unless values
+ _enum(name, values, **options)
+ end
属性とデータベースのintegerの関係を明示的にハッシュでマッピングできる。
class Conversation < ActiveRecord::Base enum :status, active: 0, archived: 1 end
🔗 rename_enum
にリネーム前/リネーム後の2つの名前を渡せるよう修正
rename_enum
にはリネーム前と後の2つの名前を渡す必要があるのに、リネーム後の名前がオプショナルなのは変だ。この修正により、
rename_table
と同様にrename_enum
にもリネーム前と後の名前をオプショナルでない形で渡すようになる。同PRより
つっつきボイス:「リネームなのにリネーム後の名前が引数で省略できちゃうのはたしかに変😆」「PostgreSQLのアダプタで修正されていますね」「そういえばPostgreSQLのenumはたしかCREATE TYPE
みたいなDDLで宣言するんでしたね」
# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L577
# Rename an existing enum type to something else.
- def rename_enum(name, **options)
- new_name = options.fetch(:to) { raise ArgumentError, ":to is required" }
+ def rename_enum(name, new_name = nil, **options)
+ new_name ||= options.fetch(:to) do
+ raise ArgumentError, "rename_enum requires two from/to name positional arguments."
+ end
参考: PostgreSQL 16ドキュメント: 8.7. 列挙型
🔗JSON.encode
でCIDR形式のIPAddr
をサポート
ActiveSupport::JSON.encode
でCIDR記法をサポート改修前:
ActiveSupport::JSON.encode(IPAddr.new("172.16.0.0/24")) # => "\"172.16.0.0\""
改修後:
ActiveSupport::JSON.encode(IPAddr.new("172.16.0.0/24")) # => "\"172.16.0.0/24\""
Taketo Takashima
同Changelogより
つっつきボイス:「IPAddr
では元々"172.16.0.0/24"のようなプレフィックス長付きの書式がサポートされていたのが、JSON.encode
すると/24
の部分がJSONの文字列から落ちていたのを修正した: 今までこの機能を使っていたらbreaking changeになりそうですね」
参考: Classless Inter-Domain Routing - Wikipedia
# activesupport/lib/active_support/core_ext/object/json.rb#L242
-class IPAddr # :nodoc:
- def as_json(options = nil)
- to_s
+unless IPAddr.method_defined?(:as_json, false)
+ # Use `IPAddr#as_json` from the IPAddr gem if the version is 1.2.7 or higher.
+ class IPAddr # :nodoc:
+ def as_json(options = nil)
+ if ipv4? && prefix == 32
+ to_s
+ elsif ipv6? && prefix == 128
+ to_s
+ else
+ "#{self}/#{prefix}"
+ end
+ end
end
end
🔗 内部のquery()
をリトライ可能にした
動機/背景
Closes: #53411
permanent_connection_checkout
が導入された(リンク切れ)とき、Active Recordは常にwith_connection
を使い、lease_connection
を決して使わない(リンク切れ)ように変更された。これにより、リクエスト中に独自に(またはgem 内で)lease_connection
を呼び出さないアプリでは、コネクションが現在のスレッド/ファイバーに固定されなくなる。必要に応じてコネクションをチェックイン/チェックアウトすることで、1回のリクエスト中にコネクションを検証する必要が生じる回数が大幅に増加していた(pingが増えるとレイテンシも増加する)。良いニュースとしては、Active Recordがコネクションの検証をdefer(先延ばし)(リンク切れ)するようになり、明示的なpingの代わりに再試行可能なクエリを検証として利用可能になったというのがある。
詳細
このコミットにより、内部のすべてのSELECT
query()
がリトライ可能になり、コネクション検証で利用可能になる。これは、スキーマキャッシュダンプを使うアプリケーションの運用ではあまり有用ではない可能性もあるが、開発中のアプリケーションやproductionでダンプを使わないアプリケーションのレイテンシは確実に短縮される。シーケンスを更新するために
query_value
が使われていた場所がいくつかあったが、これらはinternal_execute
を使う形に変更された。理由は、自動的にリトライされるのは冪等なSELECTクエリだけであり、Active Recordがこれらのクエリの戻り値を実際には気にしないため(つまりinternal_exec_query
を使う理由がない)。追加情報
このプルリクのフォローアップもさらに行うつもりだが、既に現状のプルリクが(概念上は😅)だいぶ大きくなっている。
同PRより
つっつきボイス:「lease_connection
は以前全廃されたメソッドでしたね(ウォッチ20240402)」「マルチスレッド/マルチFiberが関連してくる内部改修は大変そう」
🔗 CurrentAttributes#attributes
のバグを修正
ActiveSupport::CurrentAttributes#attributes
が呼び出しのたびに新しいハッシュオブジェクトを返すよう修正された従来は、メソッドが呼び出されるたびに同じハッシュオブジェクトが返されていた。
fatkodima
同Changelogより
CurrentAttributes
のバグと思われるものを踏んだ。自分たちは信頼性の高いプッシュ機能を備えたSidekiqを使っている。基本的にSidekiqはジョブをRedisにプッシュするが、Redisが一時的に利用できなくなった場合は、ジョブのペイロードをメモリ内キューに保存して、再起動時にプッシュする。ジョブがキューに入れられたときの
Current.attributes
が、ジョブのペイロードの一部として保存される。ただし、現在Current.attributes
は一種のグローバルオブジェクト(メモリ内で常に同じオブジェクト)であるため、新しいオブジェクトが別のCurrent.set
値でプッシュされると、(前述のメモリ内キュー内にある)古い参照が変更され、誤った値が含まれるようになった😱もちろん、これは呼び出し側で
Current.attributes.dup
すれば修正可能だが、この振る舞いが驚くべきものであるため、(Sidekiqコードベースで行われるような)対応が忘れられてしまう。これはRails側で修正する必要があると思われるため、このプルリクで修正する。
CurrentAttributes
の意外な振る舞いをもうひとつ発見した。Current.attributes # => {} Current.set(foo: 1) { } Current.attributes # => { foo: nil } <-----
おそらくバグではなさそうだが、キーの有無に依存したり、キーが存在しないのではなく
nil
値になるだけで予期しない振る舞いが発生する可能性があるため、少なくとも期待される結果ではない。了承いただければ、これも合わせて修正可能。
cc @byroot
同PRより
つっつきボイス:「ActiveSupport::CurrentAttributes
はグローバル的に属性を受け渡しできるしくみでしたね」「CurrentAttributes
は競合状態のようなものに対してとても敏感なんですが、attributes
が同じオブジェクトを返していたのはたしかにバグで、破壊的メソッドで書き換えると壊れるやつ: 呼び出し側でdup
すれば回避できるけど、おっしゃる通り"驚くべき振る舞い"ですね」
🔗 ActiveSupport::Duration
のsince
とago
のメモリアロケーションを削減
現在のCRubyバージョンでは、Hashで
Enumerable#inject
を使うと、Hash#each
が呼び出されるたびに配列が強制的にアロケーションされてしまう。Hash#each
を直接使うべき。あまりエレガントな書き方ではないが、
inject
やreduce
の仕組みを理解する必要もなくなる。簡単なベンチマーク:
require 'active_support/all' require 'benchmark' p a = ActiveSupport::Duration.build(2716147) p a.parts time = Time.current pre_alloc = GC.stat(:total_allocated_objects) puts(Benchmark.measure do 300_000.times do a.since(time) end end) post_alloc = GC.stat(:total_allocated_objects) puts "alloced=#{post_alloc - pre_alloc}"
diff --git a/before b/after --- a/before +++ b/after @@ -1,4 +1,4 @@ 1 month, 1 day, and 1 second {:months=>1, :days=>1, :seconds=>1} - 2.311823 0.032300 2.344123 ( 2.439997) -alloced=14101243 + 2.223739 0.038590 2.262329 ( 2.316780) +alloced=12601105
スピードは最大5%改善し、オブジェクト数も最大10%削減された。
これは、yjit-benchのlobsters benchmarkでアロケーションをプロファイリングしているときに発見した。
同PRより
つっつきボイス:「Enumerable#inject
だとアロケーションが発生してしまうので、スマートじゃないけどHash#each
を使う書き方に変えてメモリアロケーションを削減したのか↓」「現在のCRubyで発生するとあるので、Rubyの実装に依存しているらしい: 本当ならRubyのEnumerable#inject
で修正したいところでしょうね」「たしかに」
# activesupport/lib/active_support/duration.rb#L486
def sum(sign, time = ::Time.current)
unless time.acts_like?(:time) || time.acts_like?(:date)
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
if @parts.empty?
time.since(sign * value)
else
- @parts.inject(time) do |t, (type, number)|
- if type == :seconds
- t.since(sign * number)
- elsif type == :minutes
- t.since(sign * number * 60)
- elsif type == :hours
- t.since(sign * number * 3600)
- else
- t.advance(type => sign * number)
- end
+ @parts.each do |type, number|
+ t = time
+ time =
+ if type == :seconds
+ t.since(sign * number)
+ elsif type == :minutes
+ t.since(sign * number * 60)
+ elsif type == :hours
+ t.since(sign * number * 3600)
+ else
+ t.advance(type => sign * number)
+ end
end
+
+ time
end
end
🔗 テストコードでContent-Typeがnil
の場合の処理を修正
#47598を終了してクローズする。
動機/背景
このプルリクを作成した理由は、@rubendinhoがプルリク#47598を引き継ぐ人が求められていたため。
元の問題は、
ActionController::TestCase
テストでリクエストのContent-Typeを明示的にnil
に設定してPOSTリクエストを実行したときにRailsが発するundefined method 'to_sym' for nil
NoMethodErrorエラーが少しわかりにくいというもの。詳細
このプルリクは、
ActionController::TestCase
のContent-Typeチェックを変更して、nil
Content-Typeに配慮する。これにより、ややわかりにくいNoMethodError
ではなく、より具体的なUnknown Content-Type
エラーをraiseするようになる。元のプルリクになかったテストを追加しておいた。
同PRより
つっつきボイス:「これは改善というよりテストコードのバグ修正でしょうね: 元のコードだとmime_type
がnil
のときにシンボルに変換できなくてNoMethodError
になるので、そこから先の分岐に進んでいなかった↓」「ぼっち演算子&.
で修正したんですね」
# actionpack/lib/action_controller/test_case.rb#L111
- case content_mime_type.to_sym
+ case content_mime_type&.to_sym
when nil
raise "Unknown Content-Type: #{content_type}"
when :json
data = ActiveSupport::JSON.encode(non_path_parameters)
when :xml
data = non_path_parameters.to_xml
when :url_encoded_form
data = non_path_parameters.to_query
else
@custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
data = non_path_parameters.to_query
end
🔗Rails
🔗 RailsBump.org(Ruby Weeklyより)
つっつきボイス:「記事は、RailsBump.orgというサイトのメンテナンスとサポートをFastRuby.ioが引き受けたという話ですが、このRailsBump.orgってちょっと便利そうですね↓」
- サイト: RailsBump
「お、Railsバージョンごとのgemの互換性情報に加えて、Gemfile.lockファイルを貼り付ければ、そこで使われているgemの互換性を個別にチェックしてくれるんですね、これはよさそう👍」「RailsバージョンごとのdiffをチェックできるRailsDiff↓と合わせて使うと、Railsのアップグレードで便利そうですね」
- サイト: RailsDiff
「RailsBumpの動作を見る限りでは、gemを動的にチェックしにいっているっぽいですね」「gemspecのdependencyを取りに行っているだけならあまり意味がないだろうから、他にもチェックしているのかも: ざっと調べた限りでは、以下のcheckerというツール↓を使って互換性をチェックしているらしい」
今週は以上です。
バックナンバー(2024年度第3四半期)
週刊Railsウォッチ: Rails 8.0 Beta 1リリース、Railsのメンテナンスポリシー更新ほか(20241024)
- 20240819 Rails 7.2でメンテナンスポリシー更新、書籍『Ruby on Railsパフォーマンスアポクリファ』ほか
- 20240808後編 puma_worker_killer、bundle_update_interactive gemほか
- 20240807前編 Rails 7.2 RC1がリリース、ストリーミングのレスポンス処理をRack 3で行うほか
- 20240709 シャーディング用メソッドを追加、localsマジックコメント修正ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)