- Ruby / Rails関連
週刊Railsウォッチ: Pumaのデフォルトスレッド数変更、Rails 1.0をRuby 3.3で動かすほか(20240206前編)
こんにちは、hachi8833です。Railsガイドを7.1.3向けに更新しました。
#Railsガイド Rails 7.1 が、2024年1月16日にリリースされた Rails 7.1.3 に対応しました🆙🎉
今後もRailsの動向に合わせ、最新のガイドをお届けできるよう励んでいきます📕✨note 記事はこちら👇https://t.co/i1MkYcriuQ pic.twitter.com/noN3VxC7Uy
— Railsガイド 📕 (@RailsGuidesJP) January 24, 2024
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Pumaコンフィグのデフォルトスレッド数を5から3に変更
修正: #50450
主な変更点は、#50450で説明されているように、デフォルトのスレッド数が5から3に減らしたこと。
将来「Railsチューニングガイド」が作成される可能性もあるため、これに関連するトレードオフの要点をコメントに含めておいた。
同PRより
ここ数日間、#50450でPumaスレッド数の新しいデフォルト値の設定について広範な議論が行われた(このissueに目を通せば、Pumaコンフィグのスレッド数に関連するレイテンシvsスループットのトレードオフの詳細についてコミュニティのさまざまなメンバーが共有している知見を得られる)。この議論に基づいて、Pumaコンフィグのデフォルトスレッド数が5から3に更新された。
同公式情報より
つっつきボイス:「お〜、Pumaのデフォルトスレッド数を変更したんですね: いろんな状況が考えられるからどんな値をデフォルトに決めるかは割りと難しい問題」「issue #50450の議論が長いですね」
「最近はDockerコンテナで動かすことが多いのと、コンテナでは昔ほど大量のメモリを割り当てることはあまりなさそうな気はするので、スレッド数5だと多いのかも」「なるほど」
「あとこの手の値は、開発者がリクエストをすぐ返却するようにコードをきちんと書くことが前提になっているものなので、そのあたりをさぼって長々とブロッキングするようなコードがあるなら値を増やすことになるのかな(さすがに今どきそういう書き方はあまり望ましくありませんが)」
# (コメントを一部省略)
# railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt#L5
-max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
-min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
-threads min_threads_count, max_threads_count
-if ENV["RAILS_ENV"] == "production"
- require "concurrent-ruby"
- worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
- workers worker_count if worker_count > 1
-end
+rails_env = ENV.fetch("RAILS_ENV", "development")
+threads_count = ENV.fetch("RAILS_MAX_THREADS") { 3 }
+if rails_env == "production"
+ processors_count = Integer(ENV.fetch("WEB_CONCURRENCY") { 1 })
+ if processors_count > 1
+ workers worker_count
+ else
+ preload_app!
+ end
+end
# Specifies the `environment` that Puma will run in.
-environment ENV.fetch("RAILS_ENV") { "development" }
-
-# Specifies the `pidfile` that Puma will use.
-if ENV["PIDFILE"]
- pidfile ENV["PIDFILE"]
-else
- pidfile "tmp/pids/server.pid" if ENV.fetch("RAILS_ENV", "development") == "development"
-end
+environment rails_env
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
+
+pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
+
+if rails_env == "development"
+ worker_timeout 3600
+end
↑development環境ではデバッグの邪魔にならないようworker_timeout
が長めに設定されています。
🔗 Object#with
に渡すブロックのインスタンスをyield
するようになった
以下のように
Object#with
に渡したブロックのインスタンスをyield
するようになった。client.with(timeout: 5_000) do |c| c.get("/commits") end
Sean Doyle
同Changelogより
Object#with
にブロック引数を導入するということは、このメソッドが以下のようにSymbol#to_proc
をブロック引数として受け取れるということ。client.with(timeout: 5_000, &:ping)
同PRより
つっつきボイス:「Object#with
は昨年Active Supportに追加されたメソッドでしたね(ウォッチ20230328)」「yield
がyield self
に変更されているんですね↓」「Object#with
はまだ使いこなせていないけど、汎用性が高まるのはよさそう👍」
# activesupport/lib/active_support/core_ext/object/with.rb#L4
# Set and restore public attributes around a block.
#
# client.timeout # => 5
- # client.with(timeout: 1) do
- # client.timeout # => 1
+ # client.with(timeout: 1) do |c|
+ # c.timeout # => 1
# end
# client.timeout # => 5
#
+ # The receiver is yielded to the provided block.
+ #
# (略)
def with(**attributes)
old_values = {}
begin
attributes.each do |key, value|
old_values[key] = public_send(key)
public_send("#{key}=", value)
end
- yield
+ yield self
ensure
old_values.each do |key, old_value|
public_send("#{key}=", old_value)
end
end
end
参考: Rails API Object#with
DHHは以下のように、Ruby 3.4で入る予定のit
が使えるのもよいと賛成していますね。
client.with(timeout: 5_000) { it.ping }
🔗 last
、pluck
、count
などでもexplain
を使えるようになった
- PR: Add
explain
support for methods likelast
,pluck
andcount
by p8 · Pull Request #50482 · rails/rails
動機/背景
リレーションで
explain
を呼び出すことで、データベースがクエリを実行する方法を調べられる。
現在のexplain
は、last
、pluck
、count
(これらはリレーションではなく実際の結果を返す)を使うクエリでは利用できないため、これらのクエリの最適化がやりにくい。
explain
で代わりにプロキシを返すようにすることで、これらのメソッドをサポート可能にする。詳細
User.all.explain.count # => "EXPLAIN SELECT COUNT(*) FROM `users`" User.all.explain.maximum(:id) # => "EXPLAIN SELECT MAX(`users`.`id`) FROM `users`"
これによって既存の動作が破壊され、以下のように
explain
に続けてinspect
を呼び出す必要が生じる。User.all.explain.inspect # => "EXPLAIN SELECT `users`.* FROM `users`"
ただし
explain
はほとんどの場合コマンドラインで使われるのと、IRBでは自動的にinspect
が呼ばれるので、これは問題にはならない。追加情報
これは、#50424でプロキシとメソッド呼び出しの代わりに引数を使う方法のバリエーションである。
代わりにブロックを渡すバリエーションも考えられる。これは既存の振る舞いを壊さない代わりに、やや冗長になる。
User.all.explain do |relation| relation.count end
&:
によるシンボル->プロック記法を使えばもう少し簡潔に書ける。User.all.explain(&:count)
この記法は、プロキシによるソリューションと異なり、
to_sql
でも応用可能。User.all.to_sql(&:count)
またはメソッド名のバリエーションを使う方法も考えられるが、
explain(:analyze, :buffers)
のようにexplain
に渡せる引数が壊れてしまうだろう。User.all.explain_count User.all.explain_maximum(:id)
同PRより
つっつきボイス:「inspect
、average
、count
、first
/last
、maximum
/minimum
、pluck
、sum
で使えるようになったんですね↓」「クエリログからSQLをコピペしてEXPLAINを付けて実行しなくてもできるのは便利👍」「軽くbreaking changeになっていて今後はexplain
に続けてinspect
を呼ばないといけないけど、IRBでinspect
が自動で呼ばれるから大丈夫だろうと言ってますね」「たしかにexplain
はIRBでクエリを調べるときに使うのがほとんどですね」
# activerecord/lib/active_record/relation.rb#L3
module ActiveRecord
# = Active Record \Relation
class Relation
+ class ExplainProxy # :nodoc:
+ def initialize(relation, options)
+ @relation = relation
+ @options = options
+ end
+
+ def inspect
+ exec_explain { @relation.send(:exec_queries) }
+ end
+
+ def average(column_name)
+ exec_explain { @relation.average(column_name) }
+ end
+
+ def count(column_name = nil)
+ exec_explain { @relation.count(column_name) }
+ end
+
+ def first(limit = nil)
+ exec_explain { @relation.first(limit) }
+ end
+
+ def last(limit = nil)
+ exec_explain { @relation.last(limit) }
+ end
+
+ def maximum(column_name)
+ exec_explain { @relation.maximum(column_name) }
+ end
+
+ def minimum(column_name)
+ exec_explain { @relation.minimum(column_name) }
+ end
+
+ def pluck(*column_names)
+ exec_explain { @relation.pluck(*column_names) }
+ end
+
+ def sum(identity_or_column = nil)
+ exec_explain { @relation.sum(identity_or_column) }
+ end
+
+ private
+ def exec_explain(&block)
+ @relation.exec_explain(@relation.collecting_queries_for_explain { block.call }, @options)
+ end
+ end
🔗 production環境ではserver.pidファイルを生成しないようになった
- PR: Do not generate pidfile in production environments by hschne · Pull Request #50644 · rails/rails
動機/背景
#50628の続き。
RailsアプリケーションをDockerで実行している場合、(メモリ不足などで) コンテナがクラッシュすると、
/rails/tmp/pids/server.pid
ファイルがすでに存在しているために再起動が失敗することがある。このような障害が発生したコンテナを再起動して実行できるようにするには、手動による介入(障害が発生したコンテナの削除など)が必要になる。コンテナを再起動可能にするには、コンテナ起動時にserver.pidファイルを削除する必要がある。
docker-entrypoint
内のファイルを削除するのではなく、#50628で提案されているように、まず新規Railsアプリケーションがproductionではデフォルトでpidを作成しないようにする必要がある。このプルリクを作成した理由は、production環境でpidファイルを作成しないようにするため。
詳細
このプルリクは、デフォルトの
puma.tt
テンプレートでpidfile
インストラクションを条件付きで生成する。ダミーアプリとテストアプリのpumaファイルも一貫性を保つために更新した。サーバーコマンド内のpidfile
のデフォルトパスも条件付きに変更した。追加情報
自分が
pidfile
を削除するのではなくpidfile
を条件付きで生成する方法を選んだ理由は、development環境で既に使われているから(例: Dockerをデフォルトで使わずに実行する場合)。この条件付きの振る舞いが混乱につながる可能性がもしかするとあるかもしれないが、心当たりはあるだろうか?
同PRより
つっつきボイス:「production環境ではserver.pidを生成しないようにしたそうです」「言われてみれば現代ではpidファイルが必要になることが減ってきたのかも」
「server.pidのように実行中のプロセスのpid(プロセスID)を保存するファイルを生成するのは、Railsに限らず昔からUnix系で広く行われているんですよ: 元々は、たとえばRailsサーバーが複数ある状態で特定のRailsサーバーを停止しようとすると、プロセスがいくつもあってどれを止めたらいいのかわからなくなるので、現在起動中のプロセスのpidをファイルに保存するようにして、スクリプトからそれを参照して止めるのに使ったりしますね」「なるほど、server.pidというと、Railsがクラッシュしたときにtmpの下に残っていて、知らずにRailsを再起動しようとすると"pidが既にあります"って邪魔されるときしか意識したことがなかったけど、そういう目的だったんですね」
「同じ名前のプロセスが複数あると、ps
コマンドなどではどのプロセスを止めればいいかを確実に特定するのは不可能なので、Unix系アプリケーションではサーバープロセス実行時のpidファイルとして生成することが昔から行われているんですよ」「そうそう、昔からシェルスクリプトから起動を制御するときはこうやってましたね」「pidを消し忘れて起動できなくなるのも昔からよくありますし、消し忘れのときはstart
ではなくrestart
することでpidファイルを削除するという実装も昔ながらのinit.dスクリプトなどでよく使われている方法ですね」
「で、現代のようにアプリケーションをDockerコンテナ内で実行するようになってくると、pidを調べなくてもコンテナを停止すれば済むので、以前ほどserver.pidの必要性はなくなってきているとも言えそう」
🔗 after_commit
コールバックやafter_rollback
コールバックでon: :update
などを指定可能になった
- PR: Fix [#50260] Support
:on
option in#set_callback
by joshuay03 · Pull Request #50261 · rails/rails
ActiveRecord::Transactions::ClassMethods#set_callback
を導入。このクラスメソッドは、
after_commit
コールバックやafter_rollback
コールバックで:on
オプションをサポートする他は、ActiveSupport::Callbacks::ClassMethods#set_callback
と同一。Joshua Young
同Changelogより
動機/背景
#50260の修正を試みるため。
詳細
ActiveRecord::Transactions::ClassMethods#set_callback
を導入する。
これは、#after_commit
コールバックや#after_rollback
コールバックで:on
オプションを利用可能にするActiveSupport::Callbacks::ClassMethods#set_callback
と振る舞いが似ている。
同PRより
つっつきボイス:「従来からあるafter_update_commit
やafter_rollback
と同じことをafter_commit
とon: :update
オプションでもできるようになったらしい↓」
# activerecord/test/cases/transaction_callbacks_test.rb#L1049
class TopicWithCallbacksOnUpdate < TopicWithHistory
after_commit :after_commit_on_update_1, on: :update
after_update_commit :after_commit_on_update_2
private
def after_commit_on_update_1
self.class.history << :after_commit_on_update_1
end
def after_commit_on_update_2
self.class.history << :after_commit_on_update_2
end
end
参考: Rails API after_update_commit
-- ActiveRecord::Transactions::ClassMethods
参考: Rails API after_rollback
-- ActiveRecord::Transactions::ClassMethods
🔗 ActiveSupport::CurrentAttributes.attribute
でdefault:
オプションをサポート
ActiveSupport::CurrentAttributes.attribute
でdefault:
オプションをサポート。class Current < ActiveSupport::CurrentAttributes attribute :counter, default: 0 end
Sean Doyle
同Changelogより
つっつきボイス:「お〜、CurrentAttributes
の属性にデフォルト値を指定できるようになったんですね」
「ところでCurrentAttributes
というと以下の記事を思い出しますけど、昨年のKaigi on Rails 2023でEvil MartiansのSampoさんがCurrentAttributes
には使いどころがあるよという発表もしていましたね」
「スライドにもあるようにコードがわかりにくくなったりテストしにくくなったりすることが多いので、CurrentAttributes
のようなグローバルコンテキストを保持するものは基本的にはあまりよくないんですが、これを使うと圧倒的に簡単に書ける機能もあるので、注意して扱うなら別に使ってもよいと思います」「なるほど」
「たとえばロガーを参照するとかリクエストオブジェクトを取りにいく機能なんかを書くときはグローバルコンテキストが取れるととっても楽ですし、Rails.logger
は実質グローバルコンテキストから取り出していたりしますね」
「昔使ってたPHPのSymfonyというMVCフレームワークではsfContext
というグローバルコンテキストがあちこちで引き渡されるようになっていて、loggerやrequestなどを任意の場所から参照しにいけるようになっていた記憶があります(今のバージョンはわかりませんが): そのぐらいロガーはどこからでもアクセスしたいものなんですよ」
参考: symfony の内側 (symfony 1.4 legacy version)
🔗 bunを使うアプリがGitHub CIでbunをセットアップするよう修正
bunを有効にして生成したアプリが、GitHub CIで
oven-sh/setup-bun
を使うよう修正。TangRufus
同Changelogより
動機/背景
bunを有効にしてアプリを生成するとGitHub CIテストがCommand install failed, ensure bun is installed
で失敗する。詳細
このプルリクは、bunを有効にして生成したアプリの
ci.yml
にoven-sh/setup-bun
を追加する。
同PRより
つっつきボイス:「bunは最近Railsで選択可能になった新しいJavaScriptバンドラーですね(ウォッチ20230926)」「JSバンドラーが多いとサポートが大変そう」「bunとかesbuildとかいろいろありますよね」
参考: §2 npmパッケージをJavaScriptバンドラーで追加する -- Rails で JavaScript を利用する - Railsガイド
🔗 オートロードパスにないファイルの再読み込みをdevelopment環境で防止するよう修正
developmentモードでアプリケーションを不必要に再読み込みしないようになった。
従来は、オートロードパスの外にある一部のファイルによって不要な再読み込みがトリガーされていた。この修正によって、アプリケーションが
Rails.autoloaders.main.dirs
に沿って再読み込みされるようになり、不要な再読み込みを防止する。Takumasa Ochi
同Changelogより
つっつきボイス:「developmentでオートロードパスの外にあるファイルが読み込まれてたんですね」「読み込まなくて済むならしない方がいいでしょうね」
# railties/lib/rails/application.rb#L412
def watchable_args # :nodoc:
files, dirs = config.watchable_files.dup, config.watchable_dirs.dup
- ActiveSupport::Dependencies.autoload_paths.each do |path|
- File.file?(path) ? files << path.to_s : dirs[path.to_s] = [:rb]
+ Rails.autoloaders.main.dirs.each do |path|
+ dirs[path.to_s] = [:rb]
end
[files, dirs]
end
参考: § 17 Rails.autoloaders
-- Rails の自動読み込みと再読み込み - Railsガイド
🔗Rails
🔗 Rails 8でIRBプロンプトを改良
Thanks for sharing this. I opened a PR 2 days ago and it has been merged.
In Rails 8 the prompt will become:
```
dev:001>
test:001>
prod:001>
```https://t.co/dChOCG9TK3— Stan Lo @st0012@ruby.social (@_st0012) January 20, 2024
つっつきボイス:「Rails 8のIRBでdev:001>
のように環境をプロンプトで短く表示するようになるそうです」「st0012さんの改修なんですね」「実行環境に合わせてプロンプトを変えたいのはわかる👍」
🔗 Railsルーティングで高度な制約を使う(Ruby Weeklyより)
つっつきボイス:「こういうふうにmatches?
で制約を定義したクラスをconstraints
に渡す形でルーティングを拡張するのは↓、Railsを長くやっていれば一度は書いたことがあるはず」「ここではIPの許可リストで制約をかけているんですね」
# 同記事より
# app/constraints/ip_constraint.rb
class IpConstraint
def self.matches?(request)
allow_list = Rails.application.config.x.ips.allow_list
request.ip.in? allow_list
end
end
# config/routes.rb
authenticated :user, -> { _1.admin? } do
constraints(IpConstraint) do
namespace :admin do
resources :users
end
end
end
🔗 comma: モデルをCSVに出力するgem(Ruby Weeklyより)
つっつきボイス:「★500個超えてますね」「wikiに使い方が書かれていた」「ApplicationRecord
にこういうのを書くとCSVを出力できるようになるみたいですね↓」「CSVからの取り込みもできたらいいんだけど」
# 同リポジトリより
class Book < ApplicationRecord
# ================
# = Associations =
# ================
has_many :pages
has_one :isbn
belongs_to :publisher
# ===============
# = CSV support =
# ===============
comma do
name
description
pages :size => 'Pages'
publisher :name
isbn number_10: 'ISBN-10', number_13: 'ISBN-13'
blurb 'Summary'
end
end
🔗 Rails 1.0をRuby 3.3で動かした(Ruby Weeklyより)
つっつきボイス:「昔のRails 1.0を最新のRuby 3.3で動かしてみた記事です」「なにゆえに😆」「謎です😆」「エンコーディングの違いやらいろいろ対応して本当に動かしたのね」「懐かしい1.0起動画面」
# 同記事より: Gemfile
source "https://rubygems.org"
gem "rails", "1.0.0"
同記事より
🔗 その他Rails
「デバッグに便利な道具」にActiveRecord::QueryLogsを書きました。SQL発行時にログを追加出力できます。https://t.co/zsUG5wFoOi
— igaiga (@igaiga555) January 24, 2024
つっつきボイス:「igaigaさんの"Ruby練習帳"にコンテンツが追加されてる」「ActiveRecord::QueryLogs
はRails 7で入った機能でしたね(ウォッチ20210906)」
参考: §3 SQLクエリコメント -- Rails アプリケーションのデバッグ - Railsガイド
前編は以上です。
バックナンバー(2024年度第1四半期)
週刊Railsウォッチ: RailsコントローラのparamsはHashではない、ruby-enumほか(20240125後編)
- 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ウォッチタグ)