giftee Tech Blog

ギフティの開発を支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信しています。

遠かった Rails が "自分事" になった瞬間 〜 transaction 内で job を安全に呼べるようになるまで

eyecatch

こんにちは、ギフティでエンジニアをしている mugi です。 先日、Ruby on Rails に初めて PR を出し、マージされました。

github.com

この記事では、PR を出すきっかけからマージされるまでの流れ、そしてこの経験を通じて感じた「OSS が "自分事" になる感覚」についてお話しします。

きっかけ:Kaigi on Rails 2025 のトーク

きっかけは、Kaigi on Rails 2025 で聞いた「非同期 job を transaction 内で呼ぶなよ!絶対に呼ぶなよ!」でした。

kaigionrails.org

このトークでは、Active Record の transaction 内で perform_later を呼び出すと、transaction がコミットされる前に job が実行されてしまう問題が紹介されていました。

ActiveRecord::Base.transaction do
  user = User.create!(name: "James")
  UserCreationNotifyJob.perform_later(user:)
end

このコードでは、UserCreationNotifyJob が transaction のコミット前にキューに入り、実行される可能性があります。もし transaction がロールバックされた場合、job は存在しないユーザーに対して処理を行おうとしてしまいます。

この問題の厄介なところは、ほとんどの場合は正常に動いてしまう ことです。transaction のコミットと job の実行のタイミングが絶妙に噛み合えば問題は起きません。だからこそ、本番環境で突然「存在しないレコード」に対するエラーが発生し、再現もできず、原因特定に苦労する。そんな経験をした方も多いのではないでしょうか。

私が担当しているプロダクトでもこの問題を認識しており、ほぼすべての job に self.enqueue_after_transaction_commit = true を設定していました。 とはいえ、毎回この設定を書くのは面倒だし、Ruby on Rails がデフォルトでやってくれたらいいのに、と思いました。

調べてみると、DHH も似たことを言っていた

そこで関連する issue や PR を調べてみました。すると、enqueue_after_transaction_commit 導入時の PR で、DHH 自身が以下のようにコメントしているのを見つけました。

Yes, by default, all jobs should enqueue after the commit.

rails/rails#53375

「デフォルトで全ての job はコミット後にエンキューされるべき」という方針は示されていたものの、実際のデフォルト値は変更されていませんでした。

方針はある。でも、まだ誰もやっていない。じゃあ、自分でやってみよう。 と思いました。

PR を出すまで

そう決めてから、Ruby on Rails のコントリビューションガイド に目を通しました。

世界中の開発者が使うフレームワークに PR を出す。正直、緊張しました。「この変更、受け入れてもらえるのだろうか」という不安もありました。

でも、やらない理由を探すより、まずは出してみようと思いました。

マージされるまでの流れ

そして PR を出してから、マージされるまで約 3 ヶ月かかりました。その間のやり取りを時系列で紹介します。

2025年9月27日:PR 作成

PR を作成しました。最初の実装では、Active Record を Active Job の依存関係として追加し、enqueue_after_transaction_commit のデフォルト値を true にする、というアプローチでした。

なぜ Active Record を依存関係に追加したかというと、enqueue_after_transaction_commit = true を動作させるには Active Record の transaction コールバックが必要だからです。Ruby on Rails 上のテストを通すためにはこれが手っ取り早いと思い、そのまま出してしまいました。 今思えば、Active Job の設計思想を十分に理解せずに「動けばいい」という発想で実装してしまったなと反省しています。

2025年11月20日:jeremy からのレビュー

Rails core team の jeremy からレビューをもらいました。

-1 on the AR dep. Would either enqueue immediately if AR is unavailable, gracefully behaving as anyone would expect, or raise an exception indicating that enqueue_after_transaction_commit should be set to false.

「Active Record への強い依存を追加するのは良くない。AR がない場合は即座にエンキューするか、例外を投げるべき」という指摘でした。

これを見て「あ、やらかした...」と思いました。Active Job は Active Record なしでも使えるべきなのに、安易に依存関係を追加してしまった。確かにやりすぎでした。

2025年11月23日:フィードバックを受けて修正

フィードバックを受けて実装を修正しました。enqueue_after_transaction_commit のデフォルト値は、Active Record が定義されている場合のみ true にする、というアプローチです。

2025年12月28日:さらなる議論とマージ

Rails core team の zzak から global config の扱いについて質問があり、jeremy と zzak との間で議論が行われました。そして jeremy 自身がコードをさらに改良してくれました。

Un-deprecated the global config toggle, added docs, switched to versioned config (load_defaults "8.2" with the new framework defaults initializer), and gated config on AR load (rather than presence of the constant).

そして、APPROVE。マージされました。

遠かった Rails が "自分事" になった

ここまでの流れを振り返って、強く印象に残っていることがあります。

「あ、この人だ」という感覚

レビューをしてくれた jeremy は Rails core team の一人で、2005 年から Ruby on Rails に関わっている重鎮です。「Ruby on Rails: The Documentary」という動画で、彼が出演しているのを見たことがありました。

youtu.be

その「動画の向こう側にいた人」が、GitHub のコメント欄で自分の PR にフィードバックをくれている。不思議な感覚でした。でも同時に、「Ruby on Rails って、こうやって人が作っているんだ」というリアリティを強く感じました。

丁寧で建設的なフィードバックで、一緒に "Rails らしく" 育ててくれる

最初のレビューで「-1」と言われたときはドキッとしましたが、それは単なる否定ではなく、「こうすればいい」という方向性を示してくれるものでした。修正したらさらに議論を深めてくれて、最終的には jeremy 自身がコードを改良してくれました。

自分の要望を一緒に "Ruby on Rails らしく" 育ててくれる。これがオープンソースコミュニティの力なのだと実感しました。

遠かった Rails が、一気に "自分事" になった

今まで、Ruby on Rails は「誰かが作ってくれたもの」を「使わせてもらっている」存在でした。でも、PR がマージされた瞬間、その感覚が変わりました。

GitHub のコミットログに自分の名前が残る。世界中の Ruby on Rails アプリに、自分の変更が入っていく。「Ruby on Rails を使っている」から「Ruby on Rails を一緒に作っている」へ。遠いように思えていた Ruby on Rails が、一気に自分事になりました。

OSS は "自分たちのプロダクト" の一部

この経験を通じて、OSS に対する考え方も変わりました。

OSS は「他の人がオーナーシップを持っているだけ」で、自分たちも関われるものです。そして、自分たちのプロダクト品質を決める重要なコンポーネントの一部でもあります。「不便だな」「こうなればいいのに」と思ったら、本家に直接貢献できます。

日々使っていて気づく。カンファレンスで学んで気づく。そして本家にフィードバックする。この循環を回していくことで、自分たちのプロダクトも、OSS も、より良くなっていきます。Rails committer は雲の上の存在ではありません。同じ開発者として、GitHub で議論できます。

まとめ

Rails 8.2 では、load_defaults "8.2" を設定することで、enqueue_after_transaction_commit がデフォルトで有効になる予定です。これにより、transaction 内で job をエンキューしても、コミット後に安全にキューに入るようになります。

もし即座にエンキューしたい場合は、個別の job で self.enqueue_after_transaction_commit = false を設定することで対応できます。

最初の一歩は怖いかもしれません。でも、踏み出せば、使っているだけだった OSS が "自分事" になります。皆さんも、「使っている OSS に貢献する」という選択肢を、ぜひ考えてみてください。自分もまだ一歩踏み出したばかり。これからも続けていきます。