- Ruby / Rails関連
週刊Railsウォッチ: Active Storageのvariantsをeager loadingするメソッドが追加、Hotwire専用Discuss、AnyCable Proほか(20210621前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は公式更新情報とコミットリストのChangelogから見繕いました。Changelogに記載された変更が多めです。
- 更新情報: Active Storage audio improvements and more | Riding Rails
- コミットリスト: Comparing @{2021-06-10}...main@{2021-06-17} · rails/rails
「公式更新情報のうち最後の1つ以外は既にウォッチで取り上げていました」
🔗 Active RecordのBaseやCoreのクラス変数をActiveRecordクラスに移動して高速化
- PR: Get rid of `cattr_accessor` in `ActiveRecord::Base` by casperisfine · Pull Request #42451 · rails/rails
- PR: Get rid of `mattr_accessor` in `ActiveRecord::Core` by casperisfine · Pull Request #42445 · rails/rails
- PR: Make `legacy_connection_handling` a module instance variable by casperisfine · Pull Request #42442 · rails/rails
つっつきボイス:「cattr_accessor
やmattr_accessor
がなくなるのかと思ったら、ActiveRecord::Base
やActiveRecord::Core
で使われているcattr_accessor
やmattr_accessor
によるクラス変数を親のActiveRecord
クラスに移動するリファクタリングを行ったようですね」「mattr
のmは何でしたっけ?」「モジュールです」
参考: Rails API cattr_accessor
-- Module
参考: Rails API mattr_accessor
-- Module
詳しくは#42442を参照。
クラス変数は端的に言って遅い。ancestor(先祖)が多いクラスでは特にそうで、ActiveRecord::Base
には60ものancestorがある。
これらのうちパフォーマンスに影響するものは一握りしかないが、一貫性を保つためにも、今後コントリビュータが新しくcattr
やmattr
を追加せずに新しいパターンに沿って進めるためにも、クラス変数をすべて移行することを考えている。
ActiveRecord::Base
に残っていたのはこのプルリクで最後のはず。Active Recordの他のクラスにもあるが、それらのancestorチェインは長くないのでさほど影響しない。
ancestorチェインが長そうな他のクラスも見てみる(ActionController::Base
あたりか?)。
#42451より大意
🔗 Active Supportのvariantsでeager loadingをサポート
つっつきボイス:「variantは、100x100みたいな添付画像のサイズバリエーションでしたね」「これはわかりやすい改修: with_all_variant_records
を使えばeager loadingできる」「いいねが21個もついてますね」「ものによってはパフォーマンスに結構影響しますし、これまで自力でN+1を回避していた人たちもいると思うので、これは欲しい機能ですね👍」
現在のActive Storageではvariantトラッキング(#37901)で添付ファイルごとにvariantが存在するかどうかをチェックするクエリが走る。通常のRailsのN+1防止策(
includes
)ではこれを防止できない。
このプルリクはwith_all_variant_records
メソッドを追加し、かつincludes
がActive Storageの添付ファイルで期待どおり動作するようになる。
user.vlogs.with_all_variant_records.map do |vlog|
vlog.representation(resize: "100x100").processed
end
また、ビルトインの
has_many
スコープも更新されてvariantレコードも読み込めるようになった。したがって、現在N+1が発生している以下のようなコードはこのプルリクによってN+1が発生しなくなる。
User.where(id: user.id).with_attached_vlogs.map do |user|
user.vlogs.map do |vlog|
vlog.representation(resize: "100x100").processed
end
end
#37901の多くのコメントが修正される。これを実装するにあたり、#39397のスタイルを大いに参考にした。
同PRより大意
🔗 replicaへの書き込み自動保護を無効に
つっつきボイス:「replicaなのでマルチデータベース関連の改修ですね」「今まではRails側でreplicaをデフォルトで書き込み禁止にしていたけど、replicaの書き込み禁止はデータベース側でやるべきなので削除した: これはそのとおりですね」「あ、そういうことですか」「想像ですけど、Rails側のロジックによる書き込み禁止は、gemと絡んだりすると迂回できてしまうことがあるんじゃないかな: replicaを使う場合はデータベース側でread専用のユーザーを作ることでリードオンリーにするのが普通ですし、書き込み禁止はデータベース側でやるべきだと思います」「なるほど」
# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L139
def preventing_writes?
- return true if replica?
return ActiveRecord::Base.connection_handler.prevent_writes if ActiveRecord.legacy_connection_handling
return false if connection_klass.nil?
connection_klass.current_preventing_writes
end
この機能を使いたければ一応書き込み保護はできるが、今後は万全な保護としては扱われない。この書き込み保護はすべてのケースを正しく分類できるほど正確ではない。ユーザーは自分でreplicaを設定して書き込みを禁止し、許可されてないクエリの場合はデータベースのエラーに依存すべき。
別の解決方法: #42432
修正されるissue: #42432
同PRより大意
🔗 MySQLアダプタのクエリパラメータのセキュリティを向上
- MySQLアダプタが、文字列の
?
で渡される数値やbooleanのパラメータを安全上の理由で文字列にキャストするようになった。あるクエリ内で文字列と数値を比較する場合、MySQLは文字列を数値に変換する。つまり、たとえば
"foo" = 0
は暗黙で"foo"
を0
にキャストしてTRUE
と評価する。これはセキュリティ上の脆弱性につながる可能性がある。
Active Recordには、比較されるカラムの型を認識している場合の脆弱性に対する保護は既にあるが、?
で渡す場合は引き続き脆弱だった。
User.where("login_token = ?", 0).first
上は以下を実行してしまう。
SELECT * FROM `users` WHERE `login_token` = 0 LIMIT 1;
修正後は以下を実行するようになる。
SELECT * FROM `users` WHERE `login_token` = '0' LIMIT 1;
Jean Boussier
同Changelogより大意
つっつきボイス:「MySQLアダプタでquote_bound_value
を追加して数値やブーリアン値を文字列にするようにしたんですね↓」「なるほど」「これまでもUser.where(login_token: 0).first
のようにハッシュの形で渡せば正しく文字列に変換されていたんですが、User.where("login_token = ?", 0).first
のように文字列とプレースホルダ?
で値を渡すときはそうなっていなくてMySQLのキャストが効いていたらしい: 従来の挙動は普通にバグっぽいですね」
# activerecord/lib/active_record/connection_adapters/mysql/quoting.rb#L6
module ConnectionAdapters
module MySQL
module Quoting # :nodoc:
+ def quote_bound_value(value)
+ case value
+ when Numeric
+ _quote(value.to_s)
+ when BigDecimal
+ _quote(value.to_s("F"))
+ when true
+ "'1'"
+ when false
+ "'0'"
+ else
+ _quote(value)
+ end
+ end
+
「ちなみにPostgreSQLだとこのようにカラムを文字列で指定するとデフォルトではクエリパーサーの段階でエラーになるんですよ(自動で型キャストする設定を付ければ変えられます)」「なるほど、ところで文中でbound variableとあるのはどういう意味なんでしょう?」「ここではプレースホルダ?
で変数の値を渡すことを指していると思います」
🔗 Model.update!
が追加
つっつきボイス:「Model.update
やsave!
は前からあったけど、エラーをraiseするModel.update!
も追加されたんですね」
# activerecord/lib/active_record/persistence.rb#L336
+ def update!(id = :all, attributes)
+ if id.is_a?(Array)
+ if id.any?(ActiveRecord::Base)
+ raise ArgumentError,
+ "You are passing an array of ActiveRecord::Base instances to `update`. " \
+ "Please pass the ids of the objects by calling `pluck(:id)` or `map(&:id)`."
+ end
+ id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
+ object.update!(attributes[idx])
+ }
+ elsif id == :all
+ all.each { |record| record.update!(attributes) }
+ else
+ if ActiveRecord::Base === id
+ raise ArgumentError,
+ "You are passing an instance of ActiveRecord::Base to `update`. " \
+ "Please pass the id of the object by calling `.id`."
+ end
+ object = find(id)
+ object.update!(attributes)
+ object
+ end
+ end
+
参考: Rails API update
-- ActiveRecord::Persistence
🔗Rails
🔗 GitLab RunnerパッケージのGPGキーローテーション
つっつきボイス:「GitLabのセキュリティポリシーに基づいてGPGキーをローテーションしたそうです」「こういう暗号化キーのローテーションは定期的にも行われますね: GitLab Runnerのパッケージが対象なので、GitLab RunnerをパッケージアップデートしようとしたときにGPG検証エラーが出るようになると思います」「ふむふむ」
「GPG keyの有効期限が来ると新しいパッケージリストのアップデートが失敗するようになるので、新しいGPG keyで署名したパッケージをインストールする際には何らかの形で事前に新しいGPG keyをインストールしておく必要があります: そうしないとインストール時にGPG署名エラーでインストールできなくなってしまう」「なるほど」
GitLabでは、GitLab Runnerの公式パッケージへの署名にGPGキーを使っています。最近、この鍵や、GitLab Runnerの公式パッケージやバイナリを配布するための他のトークンが、GitLabのセキュリティポリシーに基づいて保護されていない事例があることが判明しました。
これまで弊社は、パッケージの不正な変更や、パッケージを保存しているサービスへのアクセスの証拠を発見していません。弊社チームの調査では、整合性ハッシュ、バケットのログとバージョン管理、パイプラインの履歴などを監査した結果、パッケージが不正に変更された可能性は極めて低いと結論づけました。
慎重を期して、リリースの署名や検証に用いられていたGPGキーは、不適切に保護されていた他のすべてのトークンとともにローテーションされました。
同記事冒頭より大意
🔗 AnyCableのPro版登場
- 元記事: AnyCable Goes Pro: Fast WebSockets for Ruby, at scale — Martian Chronicles, Evil Martians’ team blog
- サイト: AnyCable: build lightning fast, reliable real-time applications with Ruby and Rails
つっつきボイス:「AnyCableのPro版が出た🎉」「Evil Martiansの記事でもプロダクションレベルのRailsサンプルアプリとしてよく使われていますね↓」
「ところでAnyCableやSidekiqのようなソフトウェアの有償版ってどのぐらい使われているのかな? AnyCableはまずEvil Martians自身が使う製品という感じもするので、売れ行きはそれほど気にしないスタンスなのかもしれませんが」「AnyCableの場合はサポートを有料で買えるみたいですね」「少なくともSidekiqのエンタープライズ版を使ってる人は見たことないですね」
参考: Sidekiq -- Simple, efficient background jobs for Ruby.
以下はつっつき後に見つけたツイートです。
Big news: AnyCable Pro is out 🚀 and inviting beta users now! Another commercial open-source bird flew out of its martian nest. Helping make it real is making me SO happy 😌 Check out https://t.co/gJNMCtp6lk https://t.co/0Kakzqqqin
— Irina Nazarova (@inazarova) June 16, 2021
🔗 Hotwire専用Discuss
今レスが一番多いのは以下のスレッドでした。
つっつきボイス:「ruby-jp Slackで知りました」「discuss.hotwire.devという専用ドメインでDiscussアプリを立ち上げたのは、HotwireがRailsに限定しないフレームワークだからかもしれませんね」「そういえばDjangoの話題(PythonのWebフレームワーク)も出ていました」
参考: Django ドキュメント | Django ドキュメント | Django
🔗 Rack::Sendfile
ミドルウェアにリクエストヘッダーからのRegexインジェクション問題
参考: rack/sendfile.rb at v2.2.2 · rack/rack
参考: Rails API send_file
-- ActionController::DataStreaming
つっつきボイス:「記事にもある、Rack::SendFile
ミドルウェアをRailsのデフォルトから削除しようというissue↓は少し前につっつきでも話題にしましたね」「あのときは情報が少なかったので記事にしませんでしたが、その後で上の情報がhackerone.comで正式に公開されました」
参考: Remove Rack::SendFile from default middleware. · Issue #41148 · rails/rails
「こういうふうにX-Sendfile-type
やX-Accel-Mapping
で正規表現を注入してReDoS(正規表現DoS)したり、読み出してはいけないファイルを読み出したりできてしまうらしい↓」
# hackerone.comより
curl -i -H 'X-Sendfile-type:X-Accel-Redirect' -H 'X-Accel-Mapping:(([^\r])+.)+[^\r]([\r])+=/www/' http://localhost:3000/files
# hackerone.comより
curl -i -H 'X-Sendfile-type:X-Accel-Redirect' -H 'X-Accel-Mapping:/.*=/secret_internal' http://localhost:80/rails/files
「tenderloveさんのコメントには、Rack::SendFile
は本来アプリケーションをプロキシの向こうに置いて使うべきもので、Railsのデフォルトミドルウェアから削除すべきだろうと書かれていました」「たしかRack::SendFile
はもともと、ファイルのダウンロードをnginxなどに中継させるのに使ったりするミドルウェアだったと思います」
Sendfileミドルウェアは、bodyがファイルから提供されるレスポンスをインターセプトして、サーバー固有のX-Sendfileヘッダーで置き換えます。Webサーバはファイルの内容をクライアントに書き込む役割を果たします。これによりRubyのバックエンドで必要な負荷を大きく減らすことができ、最適化されたファイル配信コードをWebサーバで利用できるようになります。
このミドルウェアを利用するには、レスポンスのbodyがto_path
に応答し、リクエストにX-Sendfile-Type
ヘッダが含まれている必要があります。Rack::Files
や他のコンポーネントはto_path
を実装しているので、アプリケーション内で何かをする必要はほとんどありません。通常、X-Sendfile-Type
ヘッダの設定はウェブサーバで設定します。
Rack::Sendfileドキュメントより大意
「Rack::SendFile
のようにファイルダウンロードをミドルウェアでリダイレクトする方がたしかに効率はいいんですが、RailsとWebサーバーの間でロジックをやりとりするとこのような問題が生じる可能性もありそうですね」「なるほど」
ひとまず自分のRailsアプリからはRack::SendFile
を削除しました。
$ .bin/rails middleware
use Rack::MiniProfiler
use Sqreen::ShrinkWrap
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Sqreen::Middleware
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use BetterErrors::Middleware
use Sqreen::ErrorHandlingMiddleware
use Sqreen::RailsMiddleware
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CacheStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Rack::Attack
use Rack::Attack
use Bullet::Rack
run Enno::Application.routes
🔗 その他Rails
つっつきボイス:「Parameters.new
にcontextを渡せるようにして、パラメータがunpermittedの場合にどのコントローラのどのアクションでunpermittedなパラメータが渡されたかをInstrumentationのログから取れるようになったんですね↓👍」「今まではunpermittedだったときのキーしか取れなかったそうです」
# 同記事より
context = { controller: self.class.name, action: action_name }
request_params = { user: { name: "Francesco", email: "fransceso@example.com", role: "admin" } }
params = ActionController::Parameters.new(request_params, context)
params.permit(user: [:name, :email])
# Unpermitted parameter: :role. Context: { controller: UsersController, action: create }
参考: Active Support の Instrumentation 機能 - Railsガイド
前編は以上です。
バックナンバー(2021年度第2四半期)
週刊Railsウォッチ(20210615後編)RubyのRBSを理解する、シンボルがGCされないとき、Terraform 1.0リリースほか
- 20210608後編 RubyでAppleのLZFSE圧縮データ解凍、AWS Lambda Extensionsが正式リリース、unixgame.ioほか
- 20210607前編 ActiveRecord::Relationのone?とmany?が高速化、RubyKaigi Takeout 2021登壇者募集開始ほか
- 20210601後編 Python使いから見たRuby、MySQLのインデックス解説、GitHubが採用したOpenTelemetryほか
- 20210531前編 RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか
- 20210525後編 Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか
- 20210524前編 Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか
- 20210518後編 RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか
- 20210517前編 Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210510前編 属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか
- 20210427後編 RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか
- 20210420後編 ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか
- 20210419前編 RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか
- 20210413後編 RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか
- 20210412前編 Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか
- 20210407後編 エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか
- 20210406前編 GitHubが修正したRailsセッションハンドリングの競合、erb/haml/slimの速度比較ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)