- Ruby / Rails関連
週刊Railsウォッチ: bin/ciが追加、HashWithIndifferentAccessへの変換がスキップ可能にほか(20250409)
こんにちは、hachi8833です。
Findy Conferenceがリリースされました🥳
「行きたかったのにチケットが売り切れていた😭」「スポンサー募集の〆切終わってた😭」なんてことがなくなる!!!https://t.co/RZCUCW8Rcy— まっきー (@ayamakkie) March 27, 2025
つっつきボイス:「上のリンクは、今どんなカンファレンスが予定されているのかを言語ごとやテーマごとに串刺しで表示してくれるサービスだそうです(例: Rubyの場合)」「お、コロナ後でイベントも順調に復活しているようですね: 当てもなくカンファレンスや勉強会を探すよりは、こういうサービスを手がかりに探してみてもよさそう👍」
🔗Rails: 先週の改修(Rails公式ニュースより)
- 公式更新情報: Ruby on Rails — Continuous integration at your fingertips
- 公式更新情報: Ruby on Rails — Routes Lookup Optimizations, PostgreSQL Alter Table improvements and more!
🔗 bin/ci
が追加
bin/ci
を導入することで、ワークフローステップをconfig/ci.rb
で宣言する新しいDSLに基づいてCIワークフローを標準化する。
bin/ci
は、あらゆるテスト、linter、セキュリティスキャナーを実行できる。また、必要であればプルリクにグリーン(パス)ステータスを与える形で作業を承認する。これは、プルリクなどをマージする直前に、主要な作業を完了した後の最終チェックに用いる。日常の開発/テストには、ターゲットを絞った
rails t ...
を使うこと。gh-signoffと併用すれば、
bin/ci
が正常に完了したときにプルリクに「グリーン」ステータスを設定できる。ローカルまたはクラウドで
bin/ci
で実行可能なconfig/ci.rb
定義の例:CI.run do step "Setup", "bin/setup --skip-server" step "Style: Ruby", "bin/rubocop" step "Security: Brakeman code analysis", "bin/brakeman", "--quiet", "--no-pager", "--exit-on-warn", "--exit-on-error" step "Security: Importmap vulnerability audit", "bin/importmap", "audit" step "Tests: Rails", "bin/rails", "test", "test:system" step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" if success? step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" else failure "Signoff: Failed. Do not merge or deploy.", "Fix the issues and try again." end end
▶出力例(クリックで展開)
./bin/ci Continuous Integration Running tests, style checks, and security audits Setup bin/setup --skip-server == Installing ruby == mise all runtimes are installed == Installing dependencies == The Gemfile's dependencies are satisfied == Preparing database == == Removing old logs and tempfiles == == Now start developing with bin/dev ✅ Setup passed in 3.33s Style: Ruby bin/rubocop Inspecting 563 files ................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... 563 files inspected, no offenses detected ✅ Style: Ruby passed in 1.07s Security: Brakeman code analysis bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error == Brakeman Report == Application Path: /home/dhh/Work/basecamp/know_it_all Rails Version: 8.1.0.alpha Brakeman Version: 6.2.2 Scan Date: 2025-03-06 12:16:47 +0100 Duration: 1.273865987 seconds Checks Run: BasicAuth, BasicAuthTimingAttack, CSRFTokenForgeryCVE, ContentTag, CookieSerialization, CreateWith, CrossSiteScripting, DefaultRoutes, Deserialize, DetailedExceptions, DigestDoS, DynamicFinders, EOLRails, EOLRuby, EscapeFunction, Evaluation, Execute, FileAccess, FileDisclosure, FilterSkipping, ForgerySetting, HeaderDoS, I18nXSS, JRubyXML, JSONEncoding, JSONEntityEscape, JSONParsing, LinkTo, LinkToHref, MailTo, MassAssignment, MimeTypeDoS, ModelAttrAccessible, ModelAttributes, ModelSerialize, NestedAttributes, NestedAttributesBypass, NumberToCurrency, PageCachingCVE, Pathname, PermitAttributes, QuoteTableName, Ransack, Redirect, RegexDoS, Render, RenderDoS, RenderInline, ResponseSplitting, RouteDoS, SQL, SQLCVEs, SSLVerify, SafeBufferManipulation, SanitizeConfigCve, SanitizeMethods, SelectTag, SelectVulnerability, Send, SendFile, SessionManipulation, SessionSettings, SimpleFormat, SingleQuotes, SkipBeforeFilter, SprocketsPathTraversal, StripTags, SymbolDoSCVE, TemplateInjection, TranslateBug, UnsafeReflection, UnsafeReflectionMethods, ValidationRegex, VerbConfusion, WeakRSAKey, WithoutProtection, XMLDoS, YAMLParsing == Overview == Controllers: 58 Models: 63 Templates: 181 Errors: 0 Security Warnings: 0 Ignored Warnings: 3 == Warning Types == No warnings found ✅ Security: Brakeman code analysis passed in 1.61s Security: Importmap vulnerability audit bin/importmap audit No vulnerable packages found ✅ Security: Importmap vulnerability audit passed in 1.40s Tests: Rails bin/rails test test:system Running 618 tests in parallel using 32 processes Run options: --seed 52099 # Running: .......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... Finished in 4.164330s, 148.4032 runs/s, 591.2115 assertions/s. 618 runs, 2462 assertions, 0 failures, 0 errors, 0 skips ✅ Tests: Rails passed in 6.64s ✅ Continuous Integration passed in 14.42s
同PRより
つっつきボイス:「binstubがまた追加されたそうです」「今度はbin/ci
が追加されたのね」「プルリクメッセージに書かれているgh-signoffって、gemかと思ったらシェルスクリプトでした」「CI実行後に✅を表示するような機能ですね」
「こういう感じでCIもRubyのDSLで管理できるようにしようという意図なんでしょうね↓」「いかにも最近のDHHが推してそうな機能ですね」「DHHはRailsを最終的にフレームワークだけでなく、開発に必要な周辺機能も含めて盛り込みたいという話なのかなと思います(Rails is Omakase)」
「CIはGitHub ActionsやCircleCI、GitLab CI、Jenkinsなどいろいろありますけど、それぞれに設定方法が異なるのも面倒だし、特定のCIサービスにロックインされたくないという意図もあるかもしれませんね」「なるほど」
# 同PRより
CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Security: Brakeman code analysis", "bin/brakeman", "--quiet", "--no-pager", "--exit-on-warn", "--exit-on-error"
step "Security: Importmap vulnerability audit", "bin/importmap", "audit"
step "Tests: Rails", "bin/rails", "test", "test:system"
step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
else
failure "Signoff: Failed. Do not merge or deploy.", "Fix the issues and try again."
end
end
「今気づいたけど、bin/ci
が使えるようになるということは、CIをローカルでも検証可能になるということなので、これはかなりのメリットになりそうですね」「たしかに!そういえば今日はお休みのすろっくさんも、以前GitHub Actionsをローカルで実行するactというツールでどハマりしてたのを思い出しました↓(ウォッチ20240119)」「その手のツールは本質的に本物のGitHub Actionsと同じにはできないのでどうしても無理がありますし、CIのテストはやはり実際のCIで行うしかないと思います」「結局そうなりますよね...」
🔗 implicit_order_column
で主キー/制約の追加をバイパス可能になった
implicit_order_column
の配列で複数のカラムを指定する場合、最後の要素としてnil
を追加すれば、主キーが順序条件に追加されなくなる。これにより、生成されたクエリで使われるインデックスをより正確に制御可能になる。この機能では、指定されたカラムが完全に一意でない場合、API誤動作のリスクが生じることに注意が必要。Issy Long
同Changelogより
動機/背景
implicit_order_column
に渡される配列の最後の要素がnil
の場合は、主キーやクエリ制約を追加しないようにしたい。たとえば、self.implicit_order_column = ["author_name", nil]
はORDER BY author_name DESC
を生成し、ORDER BY author_name DESC, id DESC
は生成しない(LIMIT
の有無にかかわらず)。
implicit_order_column
が複数のカラムで並べ替えるための配列をサポートするテストがなかったので、テストを追加した。詳細
- データベースシャーディングなど、パフォーマンス上の理由から主キーが並べ替えに最適なカラムではない場合がある。そのため、デフォルトで主キーを追加するかどうかをユーザーが選択可能にする必要がある。
同PRより
つっつきボイス:「これまでファインダー系メソッドは、orderが指定されていない場合に暗黙で主キーにソート(ORDER BY)が追加されていたけど、以下の["author_name", nil]
みたいに末尾にnil
を指定することで主キーのソートを追加しないようになったのか: DBのシャーディングなどの都合でidカラムをORDERで指定して欲しくないことがあるからだと説明されてますね」「なるほど」
# activerecord/lib/active_record/relation/finder_methods.rb#1134
def test_ordering_does_not_append_primary_keys_or_query_constraints_if_passed_an_implicit_order_column_array_ending_in_nil
old_implicit_order_column = Topic.implicit_order_column
Topic.implicit_order_column = ["author_name", nil]
assert_queries_match(/ORDER BY #{Regexp.escape(quote_table_name("topics.author_name"))} DESC LIMIT/i) {
Topic.last
}
ensure
Topic.implicit_order_column = old_implicit_order_column
end
「implicit_order_column
はActive Recordのクラスメソッドで、本来はORDER BYが指定されていない場合のデフォルトを指定する機能なのか」「ここでnil
を末尾に追加するのって、ちょっと見慣れない感じのオプションですね」「見ただけだと何をしているのかわかりにくいので、あんまりイケてない感じではあるかも」
参考: Rails API implicit_order_column
-- ActiveRecord::ModelSchema
参考: Rails Edge API implicit_order_column
-- ActiveRecord::ModelSchema
- Ruby on Rails API
🔗 レンダリング後にhead
を呼び出すとraiseするようになった
- PR: Raise
DoubleRenderError
onhead
after rendering by viralpraxis · Pull Request #54655 · rails/rails
- レンダリング後に
head
が呼び出されるとAbstractController::DoubleRenderError
をraiseするようになったこの変更後は、レスポンスのbodyが設定済みの状態で
head
を呼ぶとエラーになる。class PostController < ApplicationController def index render locals: {} head :ok end end
Iaroslav Kurbatov
同Changelogより
現在は、レスポンスコードを設定するために
head
を呼び出すと、以前設定したレスポンスbodyが暗黙的に削除される(0)。このため、以下のスニペットに示すように、コストのかかる不要なレンダリングが発生する可能性がある。def index render json: User.all.as_json # expensive render head :ok # Overwrites `response_body` to `` end
この振る舞いは、
render
を2回呼び出す場合や、render
の後にredirect_to
を呼び出したりする場合の振る舞いと一致しない。どちらもDoubleRenderError
(1、2)をraiseする。この変更により、Railsをアップグレードするときに既存のエンドポイントが壊れる可能性がある。そのため、このシナリオで例外と警告のどちらを発生させるかを開発者が選択できる設定オプションを導入するのがよさそう。
同PRより
つっつきボイス:「ビューのレンダリング後にhead
を呼び出すことは普通しないし、 head
メソッドはレスポンスを返すという意味で広義のrender系メソッドとも言えると思うので、レンダリング後にhead
を呼び出したらDoubleRenderError
をraiseするのは自然だし、そうあるべきですね👍」
🔗 importmapの変更時にHTMLのetagをデフォルトで無効にするようになった
- PR: Make importmap changes invalidate HTML etags by default by dhh · Pull Request #54021 · rails/rails
従来は、importmapを使う場合はHTMLレスポンスのetagが無効になるように、Rails側で
ApplicationController
にetagを手動で追加しなければならなかった。
importmap-railsの最新バージョン(v2.1.0)にはこれを追加するマクロが用意されているため、このプルリクはデフォルトのApplicationController
にそのマクロの呼び出しを追加する。
同PRより
つっつきボイス:「何のことかなと思ったら、importmap-rails側にetag設定用のstale_when_importmap_changes
メソッドが入ったので、Rails側でetagを調整しなくてもそれを呼び出せば済むように変えたということなのね」「importmap-railsと連携するstale_when_importmap_changes
をApplicationControllerに追加するようになってる↓」
<!-- railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt#5 -->
+<%- if options.using_importmap? -%>
+
+ # Changes to the importmap will invalidate the etag for HTML responses
+ stale_when_importmap_changes
+<% end -%>
「etag周りについて詳しい情報が書かれていないのでChatGPTに聞いてみると、今までは config/importmap.rbが変更されたことをブラウザに明示するdigestを埋め込もうとすると、コントローラに以下を書いたうえで↓、」
@importmap_digest = Digest::SHA256.hexdigest(Rails.root.join("config/importmap.rb").read)
「ビューで以下のような書き方↓をする必要があったのが、これを自力実装しないで良くなったということのようです」「あ、そういうことなんですね」
<%= javascript_importmap_tags "application", nonce: true, digest: @importmap_digest %>
「どちらもDHHが改修して自分でマージしているので、改修した本人はわかるけど他の人にはわかりにくいヤツだ」「DHHの改修ってときどきこういう独り言に近いコミットメッセージがありますね」
🔗 認証機能ジェネレータでセッションコントローラのテストも生成するようになった
動機/背景
このプルリクを作成した理由は、認証機能ジェネレータの
test_unit
が、rails
コマンドの認証機能ジェネレータで生成されるコントローラの機能テストを生成しないため。
test_unit
のscaffoldジェネレータは、既にコントローラをscaffoldするときに機能テストを生成しているので、認証機能ジェネレータによって生成されたコントローラの機能テストを追加することは自然であり、一貫性も向上する。詳細
このプルリクは、
test_unit
認証機能ジェネレーターを変更して、SessionsController
機能テストを生成する。この実装は、Railsのscaffold機能テストと、37signalsのWritebookのセッションコントローラのテストおよびセッションテストヘルパーをヒントに、Railsらしい書き方になるようにしている。
同PRより
つっつきボイス:「Railsの認証機能ジェネレータでセッションコントローラのテストも生成するようになったのか」「これ個人的にありがたいかも🙏」
「セッションコントローラみたいにジェネレータが生成したものを無改造のまま使うなら、テストコードはなくてもいいといえばいいかもしれないけど、セッションコントローラに問題が起きると致命的なので、その意味ではテストコードもあっていいでしょうね👍」
# railties/lib/rails/generators/test_unit/authentication/templates/test/controllers/sessions_controller_test.rb
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
setup { @user = User.take }
test "new" do
get new_session_path
assert_response :success
end
test "create with valid credentials" do
post session_path, params: { email_address: @user.email_address, password: "password" }
assert_redirected_to root_path
assert cookies[:session_id]
end
test "create with invalid credentials" do
post session_path, params: { email_address: @user.email_address, password: "wrong" }
assert_redirected_to new_session_path
assert_nil cookies[:session_id]
end
test "destroy" do
sign_in_as(User.take)
delete session_path
assert_redirected_to new_session_path
assert_empty cookies[:session_id]
end
end
🔗 method_missing
を補完するrespond_to_missing
メソッドをActiveRecord::Migration
に追加
動機/背景
既存の
method_missing
を補完するためのrespond_to_missing?
の実装を追加する。詳細
以下のようなマイグレーションファイルがあるとする。
class AddUserIdToLists < ActiveRecord::Migration[7.1] def change add_column :lists, :user_id, :integer end end
上は問題なく動作するが、以下の結果になる。
AddUserIdToLists.new.respond_to?(:add_column) #=> false
これが発生するのは、一部のマイグレーションメソッドが明示的に定義されておらず、
method_missing
で動的に処理されるため。respond_to_missing?
を実装することで、respond_to?
を利用可能なメソッドであることが正しく反映され、一貫性と予測可能性が向上する。
同PRより
つっつきボイス:「マイグレーションのadd_column
のような動的メソッドは、マイグレーション中に問題なく呼び出せるけど、その時点ではまだmethod_missing
に応答しない、たしかに」「それでrespond_to_missing?
を追加して応答するようにしたんですね」「なくても動くけど、こういうのが欲しいケースがあったのかもしれませんね」
# activerecord/lib/active_record/migration.rb#L785
+ def respond_to_missing?(method, include_private = false)
+ return false if nearest_delegate == delegate
+ nearest_delegate.respond_to?(method, include_private)
+ end
🔗 ActiveModel::Type::Value
のカスタムユーザー型をデフォルトでミュータブルに変更した
動機/背景
ActiveModel::Type::Value
のカスタムユーザー型は、#53921以降デフォルトでイミュータブルになっていることが判明した(mutable?
はnodocである)。これが原因で、カスタム型がdup_or_share
でイミュータブルになり、適切に複製されないという予期しないシナリオに遭遇した。詳細
Value
型のmutable?
のデフォルトをtrue
に変更した。新しい内部Helpers::Immutable
によって、以前のすべてのイミュータブル型がmutable?
をfalse
に設定するようになった。
同PRより
つっつきボイス:「ユーザー型の値がイミュータブルだとdup_or_share
で複製できなくなるのでデフォルトをミュータブルにしたのか」「dup_or_share
は最近できた内部メソッドなんですね(#53921)」
🔗 HashWithIndifferentAccess
値の変換をスキップ可能にした
修正: #54433
ActiveModel::Dirty
でハッシュ値をキャストしないようにするときに使う。これは暫定的な修正であり、満足しているわけではないが、これより良いソリューションを思いつかない。意見求む>@matthewd
同PRより
つっつきボイス:「これもあんまり情報がありませんね」「issue #54433を見ると、Hash
が勝手にHashWithIndifferentAccess
に変換されてしまうことがあって、そうするとYAMLを使うシリアライズカラムでシリアライズできなくなってしまうのか」
「Railsのコアな機能の1つであるHashWithIndifferentAccess
に今になって修正が入ったりしたら、HashWithIndifferentAccess
を使っている古いgemあたりがもしかすると壊れるかもしれない🤔」「コア機能を修正するのは怖いですね...」「だからconvert_value: false
オプションを追加する形で暫定的に修正したんでしょうね: この悩ましい気持ちはわかる」
参考: Rails API ActiveSupport::HashWithIndifferentAccess
ActiveModel::AttributeMutationTracker
は変更を保存するときにActiveSupport::HashWithIndifferentAccess
を使っているため↓、Hash
が意図せずActiveSupport::HashWithIndifferentAccess
に変わってしまう。# https://github.com/rails/rails/blob/4d42d34adda4011e770c18136966cb5df08c9220/activemodel/lib/active_model/attribute_mutation_tracker.rb#L26-L32 def changes attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if change = change_to_attribute(attr_name) result.merge!(attr_name => change) end end end
これによってproductionでバグが発生する。私たちが使っている
acts_as_list
にはこのようなコード片があり、これは本質的に、保存されたダーティな変更を取り消し、いくつかのチェックを実行してから、変更を再適用している。デフォルトでは
ActiveSupport::HashWithIndifferentAccess
をシリアライズできないため、YAMLを使シリアライズカラムで利用すると、これが問題になることがある(以下の test_acts_as_list_raise を参照)。(再現コードは省略)
期待される振る舞い
おそらく
post.changes
は、ActiveSupport::HashWithIndifferentAccess
に変換されるのではなく、元のハッシュオブジェクトを返す。これは
acts_as_list
のバグとも考えられるが、このように広く使われているgemで発生するという事実は、この振る舞いが少なくとも予想外であることを示しているように思われる。また、この変換は意図的に行われているようには見えず、ActiveSupport::HashWithIndifferentAccess
が他のハッシュのマージを実行する方法とたまたま似ただけのように思える。実際の振る舞い
Hash
はActiveSupport::HashWithIndifferentAccess
に変換されてしまう。issue #54433より
🔗Rails
🔗 dd-trace-rb: DataDogのRubyプロファイラ
つっつきボイス:「このIvo AnjoさんはRubyのプロファイリング周りに強い方で、記事によるとproduction環境に常駐可能なRubyプロファイラをDataDogのgemとして1年がかりで作って公開したそうです」「お、プロファイリングはproduction環境で行うのが一番信頼できるので、これが使えることを知っておくとよさそう👍: DataDogは高いけどアプリケーション監視サービスとして長年の実績がありますね」
🔗 invert_where
が予想に反して振る舞うとき(Ruby Weeklyより)
つっつきボイス:「thoughtbotの記事です」「invert_where
は比較的最近追加されたメソッドだけど、この手のメソッドは想定通りに反転されない可能性がありうるので、すべてを掌握できているのでない限り、よさげだからという理由だけで気楽に使わない方がいいと思います」「何だかクイズみたいな振る舞いで怖いですよね」「記事ではdefault_scope
のあるモデルで気づかずにinvert_where
を使うと2つのスコープが両方とも反転されてハマるとある↓」
# 同記事より
class Claim < ApplicationRecord
default_scope { where(archived: false) }
scope :recent, -> { where("created_at >= ?", 10.days.ago) }
end
# Inverted scope
old_claims = Claim.recent.invert_where
参考: Rails API invert_where
-- ActiveRecord::QueryMethods
🔗 Debugbar: Railsデバッグ情報をブラウザdevtools風に表示(Ruby Weeklyより)
つっつきボイス:「こういう感じのツールが昔もあった気がするけど、これはLaravel-debugbarにヒントを得て作ったとある」「先輩がいたんですね」「Devtoolsをカスタマイズするのかなと思ったら、Devtools風に表示するデバッガなのね: 使いたい人はどうぞ👍」
🔗 activerecord-pretty-comparatorとハッシュ方式のクエリ構文
アクティブレコードチョットデキルパーソンなのでお得情報コメントしといた🙂↕️https://t.co/epnJbOQj5w
— Ryuta Kamizono (@kamipo) March 18, 2025
つっつきボイス:「Active Recordでおなじみkamipoさんの上のツイートを見てリンク先のコメントが最初何のことだかわからなかったので、AIにも聞いたりして調べたところ、どうやらkamipoさんはwhere
構文について従来の"id >?", 9
のようなプレースホルダ?
を使う方式よりも、"id >": 9
のようなハッシュ方式の構文を推しているらしいことを知りました↓」「たしかこの構文については随分昔から議論がありますけど、また盛り上がってきたのかな: 少なくともハッシュ方式の方が生SQLらしさが減って理解しやすくなるという良さがあるのはわかる」「なるほど」
# 従来のプレースホルダ方式
posts.where("id >?", 9).pluck(:id) # => [10, 11]
# kamipoさんが推しているハッシュ方式(activerecord-pretty-comparatorも同じ)
posts.where("id >": 9).pluck(:id) # => [10, 11]
🔗Ruby
🔗 RubyらしくValue Objectを作る(Ruby Weeklyより)
つっつきボイス:「読んでみたら例のData.define
を使う方法の解説でした」「Dataクラスで生成したオブジェクトは使いやすいし、従来のStructと違ってイミュータブル(不変)なので、Data.define
でやるのは今や定番の方法ですね👍」「Structよりも速いですし」
# 同記事より
Price = Data.define(:amount, :currency)
参考: class Data
(Ruby 3.4 リファレンスマニュアル)
🔗 その他Ruby
これから話すやつです #shibuyarb
RubyKaigi 2025 Mapを作った #rubykaigi - くりにっき https://t.co/kO33bvmd1V #RubyKaigi 2025
— sue445 (@sue445) March 19, 2025
つっつきボイス:「もう来週がRubyKaigiですが、イベント開催場所をGoogleマップで作ってくださいました🙏」「お、そういえば松山は10年以上前にRuby World Conferenceの後で寄りましたね」「そうそう、道後温泉入りましたね」(以下延々)
🔗DB
🔗 SQL Fiddle -- 複数のRDBMSでSQLを試せるサイト
つっつきボイス:「先週社内発表でこのサイトが引用されていたのを見て知りました」「画面的には昔からあったっぽいけど、AIチャットやAIでのSQL生成なんかもやっているので、自分が知ってたものと違うかも」「名前はJSFiddleのもじりっぽいですね」「機能検証をしたいときなんかにRDBMSを切り替えてさっと試せるのはよさげ👍」
参考: JSFiddle - Code Playground
🔗クラウド/コンテナ/インフラ/Serverless
🔗 Sidekiq 8.0(Ruby Weeklyより)
つっつきボイス:「そうそう、Sidekiq 8.0がリリースされましたね🎉」
「新機能としてはプロファイリングやWeb版UIの強化があるけど、ジョブのイテレーション機能が気になる↓」「Sidekiq 8より前から一応あったんですね」
イテレーション
ジョブのイテレーション(反復処理)は、長時間実行されるジョブを小さな作業単位に分割できる新機能です。この機能を使うと、個別の反復処理ステップ実行に要する時間が30秒未満であることを条件に、数日にわたって安全に実行可能なジョブを構築できます。
たとえば、数百万のアカウントのデータ移行を実行するジョブを考えてみましょう。各ステップで100件のアカウントが移行されます。
この機能はSidekiq 7.3で導入され、その後数か月にわたって改良が続けられてきました。Sidekiqのすべてのユーザーは、wikiページのイテレーションを確認して詳細を確認することを強くお勧めします。
同記事より
「リンク先のWikiに書かれているサンプルコード↓を見るとなるほどという感じ: 以下のようにIterableJob
としてジョブを生成してeach_iteration
するらしい」「なるほど」「プロセスでやっているのかスレッドでやっているのかはコードを追いかけないとわからないけど、Wikiを見る限りでは当該each iterationは1個のWorkerプロセスではなく別のWorkerにもまたがって実行されそうな感じにも見えますね(実際どうなのかはよく分からないので詳しい方の情報求む🙏)」
# https://github.com/sidekiq/sidekiq/wiki/Iterationより
class PostCreator
include Sidekiq::IterableJob
# PostCreator ジョブごとにN個のPostsを作成するが、中断可能。
# Ctrl+Cを押すと、作成中のPostが保存され、現在のカーソルも保存されて
# すぐに再開するようにスケジューリングされる。
# 再開は0からではなく、次のカーソル値から再開される。
def build_enumerator(start_at, count, **kwargs)
@start_at = start_at
@count = count
logger.info { "Creating posts for #{start_at}" }
array_enumerator((start_at...(start_at + count)).to_a, **kwargs)
end
def each_iteration(pid, *_unused_args)
Post.create!(id: pid, title: "Post #{pid}", body: "Body of post #{pid}")
end
# 1000件のPostsが作成されると、その1000件の投稿に対して一括で何らかの操作を実行可能になる。
# このジョブでは1000件のみが実行され、50,000件すべてが実行されないことに注意。
# 50,000件のPostsがすべて作成されたかどうかを知りたい場合はSidekiq Proのバッチ機能が必要。
def on_complete
logger.info { "#{@start_at} complete, updating..." }
PostUpdater.perform_async(@start_at, @count)
end
end
「each_iteration
ごとの処理は独立している前提のようなので、大きなトランザクション内でイテレーションするようにはできてなさそう」「純粋に分割可能なタスクをイテレーションするということですね」「別のサンプルコードを見るとeach_iteration
の中でトランザクションを生成しているので、たぶんそういうことだと思う」
「特定のタスクだけ失敗する可能性もあるけど、そういう場合はon_*
のようなコールバックで処理するんでしょうね: 上のon_complete
は失敗なしで完了したときだけ呼び出されるので、データベースで言う全体結果のコミット的な処理をここで行えるのは嬉しいかも」「なるほど、自力でジョブの結果をかき集めて管理しなくてもon_*
コールバックでできるんですね」
「独立したマイクロジョブが大量に生成されるような処理をキューイングするとキューが増えすぎて、たとえばすべてのジョブが正常に完了したかどうかは個別に調べなければならなくなったりするけど、おそらくこのイテレーション機能を使えば、キューの数も削減できて完了チェックなんかも扱いやすくなったりするんじゃないかな」「なるほど、この機能が合うユースケースありそうですね」「プロファイリング、Web版UI、ジョブのイテレーションは嬉しい機能だと思います👍」
今回は以上です。RubyKaigi 2025でお会いしましょう!
バックナンバー(2025年度第1四半期)
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)