Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: bin/ciが追加、HashWithIndifferentAccessへの変換がスキップ可能にほか(20250409)

こんにちは、hachi8833です。


つっつきボイス:「上のリンクは、今どんなカンファレンスが予定されているのかを言語ごとやテーマごとに串刺しで表示してくれるサービスだそうです(例: Rubyの場合)」「お、コロナ後でイベントも順調に復活しているようですね: 当てもなくカンファレンスや勉強会を探すよりは、こういうサービスを手がかりに探してみてもよさそう👍」

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やX.comでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

🔗 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実行後に✅を表示するような機能ですね」

basecamp/gh-signoff - GitHub

「こういう感じで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で行うしかないと思います」「結局そうなりますよね...」

nektos/act - GitHub

🔗 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するようになった

  • レンダリング後に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を呼び出したりする場合の振る舞いと一致しない。どちらもDoubleRenderError12)をraiseする。

この変更により、Railsをアップグレードするときに既存のエンドポイントが壊れる可能性がある。そのため、このシナリオで例外と警告のどちらを発生させるかを開発者が選択できる設定オプションを導入するのがよさそう。
同PRより


つっつきボイス:「ビューのレンダリング後にheadを呼び出すことは普通しないし、 headメソッドはレスポンスを返すという意味で広義のrender系メソッドとも言えると思うので、レンダリング後にheadを呼び出したらDoubleRenderErrorをraiseするのは自然だし、そうあるべきですね👍」

🔗 importmapの変更時にHTMLのetagをデフォルトで無効にするようになった

従来は、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の改修ってときどきこういう独り言に近いコミットメッセージがありますね」

Rails: importmap-rails gem README(翻訳)

🔗 認証機能ジェネレータでセッションコントローラのテストも生成するようになった

動機/背景

このプルリクを作成した理由は、認証機能ジェネレータの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が他のハッシュのマージを実行する方法とたまたま似ただけのように思える。

実際の振る舞い

HashActiveSupport::HashWithIndifferentAccessに変換されてしまう。

issue #54433より

🔗Rails

🔗 dd-trace-rb: DataDogのRubyプロファイラ


つっつきボイス:「このIvo AnjoさんはRubyのプロファイリング周りに強い方で、記事によるとproduction環境に常駐可能なRubyプロファイラをDataDogのgemとして1年がかりで作って公開したそうです」「お、プロファイリングはproduction環境で行うのが一番信頼できるので、これが使えることを知っておくとよさそう👍: DataDogは高いけどアプリケーション監視サービスとして長年の実績がありますね」

DataDog/dd-trace-rb - GitHub

🔗 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

Rails 7のActive Recordにinvert_whereメソッドが追加される(翻訳)

🔗 Debugbar: Railsデバッグ情報をブラウザdevtools風に表示(Ruby Weeklyより)

julienbourdeau/debugbar - GitHub


つっつきボイス:「こういう感じのツールが昔もあった気がするけど、これはLaravel-debugbarにヒントを得て作ったとある」「先輩がいたんですね」「Devtoolsをカスタマイズするのかなと思ったら、Devtools風に表示するデバッガなのね: 使いたい人はどうぞ👍」



https://debugbar.dev/より

barryvdh/laravel-debugbar - GitHub

🔗 activerecord-pretty-comparatorとハッシュ方式のクエリ構文


つっつきボイス:「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]

technuma/activerecord-pretty-comparator - GitHub

🔗Ruby

🔗 RubyらしくValue Objectを作る(Ruby Weeklyより)


つっつきボイス:「読んでみたら例のData.defineを使う方法の解説でした」「Dataクラスで生成したオブジェクトは使いやすいし、従来のStructと違ってイミュータブル(不変)なので、Data.defineでやるのは今や定番の方法ですね👍」「Structよりも速いですし」

# 同記事より
Price = Data.define(:amount, :currency)

参考: class Data (Ruby 3.4 リファレンスマニュアル)

🔗 その他Ruby


つっつきボイス:「もう来週が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がリリースされましたね🎉」

sidekiq/sidekiq - GitHub

「新機能としてはプロファイリングや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四半期)

週刊Railsウォッチ: RailsガイドがRails 8.0.1に対応ほか(20250123)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。