- Ruby / Rails関連
週刊Railsウォッチ: Rails 7.2 RC1がリリース、ストリーミングのレスポンス処理をRack 3で行うほか(20240807前編)
こんにちは、hachi8833です。間が空いてしまって恐縮です🙇。
今朝Rails 7.2 RC1がリリースされました↓。
Rails 7.2 release candidate 1: Better production defaults, Dev containers, new guides design, and more! https://t.co/KrbJhMMiBn
— Ruby on Rails (@rails) August 6, 2024
🔗Rails: 先週の改修(Rails公式ニュースより)
- 公式更新情報: Ruby on Rails — Configurable compressor for encryption, Rack 3 streaming and more
- 公式更新情報: Ruby on Rails — New Rails beta release, immutable option in http_cache_forever and more
- 公式更新情報: Ruby on Rails — Add non-null modifier for migrations, default script folder and generator, sessions generator and much more!
7月中はマイルストーンに2件ほど残っていましたが、8/1にクローズされていました↓。
参考: 7.2.0 Milestone
🔗 ActiveRecord::Encryption::Encryptor
に:compressor
オプションで圧縮アルゴリズムを渡せるようになった
ActiveRecord::Encryption::Encryptor
で:compressor
オプションがサポートされ、利用する圧縮アルゴリズムをカスタマイズ可能になった。module ZstdCompressor def self.deflate(data) Zstd.compress(data) end def self.inflate(data) Zstd.decompress(data) end end class User encrypts :name, compressor: ZstdCompressor end
compress: false
を渡すことで圧縮を無効化できる。class User encrypts :name, compress: false end
heka1024
同Changelogより
つっつきボイス: 「圧縮アルゴリズムをカスタマイズ可能にする機能って前にも入っていたような気がしたけど、キャッシュストアの圧縮のカスタマイズだった(ウォッチ20230823)」「Encryptor
クラスは数年前にEncryption
モジュールに導入されたんですね」「暗号化と圧縮は用途も実装も本来別物として考えるべきなので、こういう機能はあっていいと思います👍」
# activerecord/lib/active_record/encryption/encryptor.rb#L14
class Encryptor
+ # The compressor to use for compressing the payload
+ attr_reader :compressor
+
# === Options
#
# * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
# Defaults to +true+.
- def initialize(compress: true)
+ # * <tt>:compressor</tt> - The compressor to use.
+ # 1. If compressor is provided, it will be used.
+ # 2. If not, it will use ActiveRecord::Encryption.config.compressor which default value is +Zlib+.
+ # If you want to use a custom compressor, it must respond to +deflate+ and +inflate+.
+ def initialize(compress: true, compressor: nil)
@compress = compress
+ @compressor = compressor || ActiveRecord::Encryption.config.compressor
end
参考: Active Record と暗号化 - Railsガイド
🔗 Railsのストリーミングレスポンス処理をRack 3に移行
動機/背景
文脈については#52066を参照。
Rack 3には、「ストリーミング」のレスポンスを処理する方法を拡張するさまざまな機能が導入されている。
body#to_ary
が存在する場合は、レスポンスがバッファリング可能であることを示す(つまりアプリケーションの振る舞いに影響せずに文字列インスタンスの配列に変換される)。#each
はbodyを消費する(つまりチャンクエンコーディングを用いてインクリメンタルに送信可能なチャンクを以後生成する)。これはRack 2でもある程度有効。#each
に応答する代わりに、bodyが#call
に応答することが可能になった(これは部分的なハイジャックのように呼び出されるが、#each
と同一のセマンティクスを持つ)。Railsは現在も
transfer-encoding: chunked
でエンコードされたレスポンスをRackのbodyとして引き続き生成しているので、以下のような問題が生じる可能性がある。
- レスポンスのbodyがエンコードされるため、Rackの他のミドルウェアがレスポンスを適切に操作できなくなる可能性がある。
たとえば
rack-cache
について言及されているが、これはキャッシュストリーミングのレスポンスで絶対に可能であり、Railsの現状の実装上の制限でしかない。
transfer-encoding
はHTTP/2ではサポートされていない(すべてのレスポンスが事実上チャンク化されているとみなされ、ワイヤフォーマットでは長さをプレフィックスしたチャンクエンコーディングではなくバイナリフレーミングが使われるため)。Railsの実装はこの点において必要以上に制限されている。実装が不必要に複雑になり、しかもHTTP/1固有になっている。しかもこの複雑さは、プロトコルに適した方法で既にサーバー側で処理されている。
この変更は、明示的なtransfer-encodingを削除して、代わりにサーバーがレスポンスを適切にエンコードするようにした。
同PRより
つっつきボイス:「ストリーミングのレスポンス処理を従来はRails側で行っていたけど、Rack 3にその機能があるのでそれを使うようにしたということか、なるほど」「ストリーミングのコードがごっそり削除されていますね」
「プルリクのコメントに関連情報がみっちり書かれていた↓: レスポンスをchunkedで返すかどうかをRailsアプリケーションが気にしなくてよいように改修されたんですね👍」
このコードパスを使う既存のアプリケーションでは、レスポンスにチャンク転送エンコーディングが含まれていることが期待される。
Rackでは、特定のレスポンスをサーバー側で処理すべきかクライアント側で処理すべきかについての絶対的な要件は提供していない。Rack自身にもこれをテストする機能は存在しないし、そうすべきでもない。
Rackは、RFC3875で指定の指定に沿ったCGIモデルを大まかに元にしている。Rackサーバーは、クライアントブラウザとRackミドルウェア/アプリケーションの間のプロキシ(仲介者)と見なせるため、RFC9110で概説されている要件に従わなければならない。つまり、クライアントが 側HTTP/2で、RackアプリケーションがCGI(おおよそHTTP/1.1)の場合、HTTP/1.1レスポンスを消費して有効なHTTP/2セマンティクスにマッピングする必要がある。
RackにHTTP/2サポートの導入作業を開始したとき、このアプローチではずっと複雑になってしまい、Rackプロトコルの進化を妨げると判断した。つまり、このままでは永遠にCGIとその欠点や問題にとらわれることになる。
CGIへの依存は絶対的な要件というより利便性目的と思われるため、RackがHTTP/2をサポートする形に進化する過程で、私は共通の妥協点に向かってうまく進めるようにしてきた。言い換えれば、RackはRFC3875からRFC9110(それぞれのリンクは上記参照)へと徐々に移行中である。つまり、RackはHTTPの特定のバージョンではなく、HTTPセマンティクスに従うことになる。
この結果、可能ならRack内でHTTPバージョン固有のロジックを実行することは避け、代わりにRackが共通のセマンティクスセット(アップグレード処理用の
rack.protocol
など)を提供する必要がある。これらのセマンティクスは、可能な場合は背後のプロトコル機能に明確にマッピングする必要があるが、実際の実装はサーバーに任されている。客観的な例を1つ挙げると、Falconが
transfer-encoding: chunked
レスポンスを確認してそのレスポンスをデコードしてHTTP/2用に書き換えることは完全に可能ではあるが、このようなアプローチは非常に効率が悪く、元のエンコードに加えて以後のデコードと再エンコードのステップも発生してしまう。そのため、Rackは#to_ary
および#each
に関連する抽象的なセマンティクスを提供し、(1)手軽にバッファリングできるレスポンスを提供する方法と(2)クライアントに段階的に送信できるレスポンスを提供する方法を指定する。このクラスが削除されたため、レスポンスがチャンク化されなくなった。下流のWebサーバーはチャンク化されたレスポンスを使うかどうかをどのようにして知るのか?
サーバーは、特定のエンコーディングを使うかどうかを強制されるべきではない。接続しているクライアントにとって最も適切なものを決定するのはサーバー次第である。このロジックは、アプリケーション層(Railsの場合はフレームワーク層)に実装すべきではなく、実装における階層化違反のように思える。
アプリケーションは、接続しているクライアントの種類や、作成する最適なレスポンスの種類を気にする必要はない。Rackは、アプリケーションがネットワークプロトコルではなくビジネスロジックに専念できるように適切な抽象化を提供すべき。とは言うものの、
#to_ary
(バッファリングできない)に応答せず、#each
に応答するレスポンスbodyは、段階的に送信されることが期待される。これについて試したことがなかったので、クライアントブラウザとRackアプリケーション間のやり取りをテストするためにrack-conform
を作成した。これはエンドツーエンドの統合テストであり、特定のサーバー+アプリケーションの実際のクライアント側の動作を確認できる。プルリク#20に示されているように、サーバーが意図したとおりに動作していることを確認可能。ただしバッファリング戦略が異なる場合もありうる(つまりサーバーがチャンクを独立して送信することが常に保証されるわけではない)が、これはまったく問題なく、サーバーが決定できる範囲内であると思う。さもないと、このコードパスは現代のWebサーバーでは機能しないだろう(ただしこれを確認したわけではない)。
はっきり言って、確認に必要な作業は完了していると信じているので、必要なら自分でも確認できる。さまざまな(あらゆる主流の)サーバーで動作することを確認したい場合は、上記のテストスイートに追加のテストを自由に導入して欲しい。このあたりを詳しく見てもらえると助かる。
さらに、リクエストがHTTP/1.0の場合に特別な処理を行っているように見えるが、そのためのテストが削除されているっぽい。
上述のように、これはアプリケーションコードの問題ではなくサーバーの問題であり、ストリーミングテストで考慮すべき事項ではないため、テストを削除した。実際、HTTP/1.0でレスポンスをストリーミングすることはまったく問題ない。RailsがHTTP/1.0でストリーミングをスキップしていたのは、パフォーマンスに対する人為的な制限だった(が、現在の実状には関係ない)。
それとは別に、Railsによって生成されたチャンクを確認する追加のテストを作成するとよいだろう。
Rack::Test
はストリーミングであるかどうかに関係なくレスポンスを正しく正規化するので、ミドルウェア/サーバーが何を決定したかに関係なく、テストは同じように動く。これを変更する必要はないと思われる。むしろ、特定のコントローラーを呼び出すと特定のチャンクのリストが生成されることを確認する単体テストを用意する方がよさそう。この変更が下位互換である理由がわからない。既存のアプリが壊れない理由がよくわかる説明が欲しい。
ご存知のとおり、変更が下位互換性があるかどうかを確実に知ることは不可能だが、Railsでテストが壊れたもののストリーミングレスポンスは正常に動いた。問題はテストスイートが特殊であることで、ストリーミングが実際に行われていることを確認するテストの動作レベルは誤っていると強く思っている。コントローラーが複数のチャンクを含むbodyを返していることを確認するのがベスト。このテストは、そのような利用を想定して設計されていないメカニズムを用いて、レスポンスのワイヤ形式を確認しようとしている。
互換性の喪失への対抗策として、このプルリクによって、実際にはより幅広いストリーミング方法が可能になる。たとえば、 HTTP/1.0とHTTP/2以降が正しくサポートされるようになった。Rackが提供する正しい「抽象セマンティクス」を使うことで、サーバーにレスポンスをストリーミングする機会を提供できる。これはアプリケーション層で実行できるベストであり、そのためのテストが必要。
同PRコメントより
🔗 ETag
とLast-Modified
が両方存在する場合はETag
を優先するよう修正
config.action_dispatch.strict_freshness
を追加。このコンフィグを
true
に設定すると、RFC 7232, Section 6での指定に沿って、ETag
とLast-Modified
が両方存在する場合はETag
を優先するようになる。従来のRailsバージョンとの互換性を維持するため、デフォルトでは
false
に設定されているが、Rails 8.0からはデフォルトでtrue
になる。heka1024
同Changelogより
修正: #52191
概要
このプルリクは、RailsのHTTPキャッシュ処理の振る舞いをHTTP仕様にさらに適合するように変更する。具体的には、この変更により、リクエストが最新かどうかを判断する際に、
ETag
ヘッダーが他の条件よりも優先されるようにする。詳細
従来のロジックでは、リクエストが新規とみなされるには、
ETag
ヘッダーとLast-Modified
ヘッダーの両方が一致する必要があった。修正後の新しいロジックでは、ETag
ヘッダーが優先される。これは、RFC 7232 Section 6で提供されているガイダンスに準拠している。変更内容
HTTPのキャッシュハンドリングのコードをリファクタリングし、
ETag
ヘッダーをLast-Modified
ヘッダーよりも優先するようにし、テストを更新して新しい振る舞いを反映した。参照
同PRより
つっつきボイス:「ETag
とLast-Modified
が両方あった場合の振る舞いがRFCに厳密に沿っていなかったので修正したのね」「ドキュメントにも明記されていますね」「ETag
とLast-Modified
が両方指定された場合には、ETag
の方がよりアプリケーション側で明示的に設定する使い方をするものなので、ETag
を優先するというのは理解できる」
🔗 http_cache_forever
をデフォルトでimmutable: true
に設定
- PR: Make http_cache_forever use
immutable: true
by natematykiewicz · Pull Request #52283 · rails/rails
http_cache_forever
がimmutable: true
になるよう修正した。Nate Matykiewicz
同Changelogより
動機/背景
#52197でexpires_in
にimmutable: true
オプションが追加された。このプルリクはhttp_cache_forever
にimmutable: true
を設定した。これはプロキシされたActive Storageファイルにも影響する。詳細
immutable: true
は、http_cache_forever
のexpires_in
呼び出しで設定される。
同PRより
つっつきボイス:「キャッシュコントロールのimmutable
は以前expires_in
に追加されたオプションですね(ウォッチ20240709)」「それをhttp_cache_forever
でもデフォルトで効くようにしたんですね↓」「これでHTTPキャッシュは基本的に変更不可になる」「"プロキシされたActive Storageファイルにも影響する"とあるのは、Active Storageのデータ参照のアクションにも影響するということですね」
# actionpack/lib/action_controller/metal/conditional_get.rb#L323
def http_cache_forever(public: false)
- expires_in 100.years, public: public
+ expires_in 100.years, public: public, immutable: true
yield if stale?(etag: request.fullpath,
last_modified: Time.new(2011, 1, 1).utc,
public: public)
end
🔗 ファイルウォッチャーでi18nの読み込みパスを削減して最適化
Railtie#initialize_i18n
の読み込み時間を最適化する。
I18n.load_path
をフィルタで絞り込んで、Rails.root
の下にあるファイルだけをファイルウォッチャーに渡す。修正前は、gemに含まれているロケール(変更は生じないのでウォッチャーでは不要)も含め、すべてのロケールをウォッチャーが取り込んでいた。Nick Schwaderer
同Changelogより
つっつきボイス:「アプリケーションの実行中にgemのインストールディレクトリ内を操作することは考えにくいので
、たしかに一回読み込めば十分」「修正もシンプルですね↓」
# activesupport/lib/active_support/i18n_railtie.rb#L65
if app.config.reloading_enabled?
directories = watched_dirs_with_extensions(reloadable_paths)
- reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do
- I18n.load_path.keep_if { |p| File.exist?(p) }
+ root_load_paths = I18n.load_path.select { |path| path.start_with?(Rails.root.to_s) }
+ reloader = app.config.file_watcher.new(root_load_paths, directories) do
+ I18n.load_path.delete_if { |p| p.start_with?(Rails.root.to_s) && !File.exist?(p) }
I18n.load_path |= reloadable_paths.flat_map(&:existent)
end
🔗 子レコードが親レコードを自動保存するときのコールバックが重複実行されることがあったのを修正
- 子が
has_one
やbelongs_to
で親を自動保存する場合にコールバックが重複実行されていたのを修正修正前は、新しい子レコードを新しい関連付けられた親レコードで永続化すると、
before_validation
、after_validation
、before_save
、after_save
コールバックが2回実行されていた。修正後、これらのコールバックは期待どおりに1回だけ実行されるようになった。
Joshua Young
同Changelogより
つっつきボイス:「重複実行は明らかにバグですね: バリデーションなどは冪等に書かれることが多いと思いますが、 *_save
コールバックのように重複実行されると困る実装もあると思うので、バグ修正されるのはありがたい👍」
🔗 ジェネレータ関連2件
- セッションジェネレータを追加
データベースでトラッキングされるセッションを用いる認証システムを作るときの出発点となる。
生成コマンド
bin/rails sessions
生成されるファイル
app/models/current.rb app/models/user.rb app/models/session.rb app/controllers/sessions_controller.rb app/views/sessions/new.html.erb db/migrate/xxxxxxx_create_users.rb db/migrate/xxxxxxx_create_sessions.rb
DHH
同Changelogより
基本的なセッションジェネレーターを追加して、ユーザーがこれを元に独自の認証システムを作れるようにした。
ただし、これは考えられるすべての認証の問題に対する万能の回答となることを意図したものではない。これは、あくまで基本的なパスを明らかにし、独自の認証システムを構築するのは珍しいことではないことを示すのが目的。したがって、魔法のリンクやパスキーや2要素認証を期待しないこと。このジェネレーターはそういうものを生成しない。
同PRより
つっつきボイス:「DHHがジェネレータ周りにいろいろ手を加えていました」「Deviseのセッションを思わせるコードですね: このrate_limit
みたいなコードはたしかにセッション系のコードでよく書くので、ひな形があってもよさそう👍」「テンプレで名前がばらつきにくくなるだけでもありがたいですね」
# railties/lib/rails/generators/rails/sessions/templates/controllers/sessions_controller.rb#L1
+class SessionsController < ApplicationController
+ allow_unauthenticated_access
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
...
script
フォルダとジェネレータを追加1回だけ実行するスクリプトや汎用スクリプト(例: データのマイグレーション用スクリプトやクリーンアップ用スクリプト)を保存する
script/
フォルダをデフォルトで追加する。以下のようにスクリプト生成用の新しいジェネレータを利用可能になる。
rails generate script my_script
生成したスクリプトは以下のように実行できる。
ruby script/my_script.rb
Jerome Dalbert, Haroon Ahmed
同Changelogより
#39552がクローズされた後、この議論に対処するための新しい試み。
動機/背景
このプルリクを作成した理由は、1回しか実行しないスクリプトの置き場所が明確でないため、DHHがdiscussでサポートを表明し、
script/
をデフォルトのディレクトリとする話が復活したことによる。詳細
このプルリクは、データのマイグレーション用スクリプトやクリーンアップ用スクリプト、ベンチマーク用スクリプトなどを置ける、スクリプト用のデフォルトディレクトリを新たに作成する。スクリプトを作成する基本的なジェネレータも追加する。
追加情報
このプルリクにスクリプトのジェネレータを追加した理由:
- それ用のサポートもある程度あるらしい。
- 新しい
script/
ディレクトリとジェネレータは密接に関連しそう。script/
ディレクトリを追加することを入門ガイドで少し触れるだけでは少々物足りない感じがする。(ガイドで他にドキュメントを書ける場所が明確ではないので)ここから始めてもいいが、新しいジェネレータを追加すれば、この新しいscript/
ディレクトリの使い方を間接的にもドキュメント化できるだろう。ジェネレータが無用ならこのプルリクから削除しても構わない。逆に、ジェネレーターのみが必要なのであれば、ジェネレータは残してデフォルトの空のスクリプトフォルダを削除すればよい。
つっつきボイス:「新しいRailsアプリではRails.root
の下にscript/
フォルダがデフォルトで作られるそうです」「スクリプトファイルの置き場所が公式に決まったんですね: この種のファイルはrakeタスクで書くのが伝統だったけど、rakeタスクはいろいろ不便なのでRailsランナーのスクリプトをここに置いて実行するのでいいという気持ちはある」
🔗 app/channels/
フォルダがデフォルトから削除される
RailsではHotwireがデフォルトになったので、カスタムチャネルは多くのアプリで不要になる。カスタムチャネルが必要なアプリでも、ジェネレータでファイルを復元すれば済む。
同PRより
つっつきボイス:「scripts/
フォルダが追加されたと思ったら今度はchannels/
フォルダがデフォルトから削除されたのね: Hotwireがあるからデフォルトではchannels/
を作らなくてもドキュメントに作り方があればいいということですね」
これに関連するドキュメントの更新プルリク↑を投げたところ、すぐにマージされました。
個人的には以下↓もマージして欲しいのですが...
参考: [ci-skip][Docs][retry] Add description for CTE to querying guide by hachi8833 · Pull Request #52423 · rails/rails -- 現在オープン
前編は以上です。
バックナンバー(2024年度第2四半期)
- 20240627後編 Railsのシステムテストを単体テストに置き換えるほか
- 20240625前編 Active Recordにstrict_loading_mode追加、to_time_preserves_timezoneの扱いほか
- 20240620後編 Ruby on Jets 6.0がRailsをサポートほか
- 20240619前編 Rails 8からPropshaftがアセットパイプラインのデフォルトにほか
- 20240529 Rails 8でKamalがデフォルトのデプロイツールになるほか
- 20240514後編 Ruby/Railsのアップグレード情報をscrapboxに集約ほか
- 20240513前編 Railsコンソールが最新のIRB APIに移行、assertionless_tests_behaviorほか
- 20240426後編 Prismの歴史と現況を振り返る、Steepの"narrowing"実装の内部ドキュメントほか
- 20240425前編 RailsからOpenStructを削除、Playwrightベストプラクティスほか
- 20240423後編 Kamalはゲームチェンジャーになるか、Solid Queueで使われているfugitほか
- 20240416前編 ジョブのエンキューをトランザクション完了時まで自動先延ばしほか
- 20240410後編 SeleniumでRubyの全クラスとモジュールにRBSが追加ほか
- 20240409前編 Rails公式の"rails-new"ツールでRailsプロジェクトをセットアップほか
- 20240402 solid_queueとmission_control-jobsが正式にRailsのgemに、Rubyの"チルド"文字列ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)