`deliver_all_later`と`perform_all_later`のロード順序バグを修正
Rails 8.1でActionMailer.deliver_all_laterを呼び出すとNoMethodErrorが発生していたバグが、メソッド定義の配置ファイルを変更することで修正されました。コードの変更はなく、定義場所の移動のみです。
背景
Rails 8.1.3において、ActionMailer.deliver_all_laterがNoMethodErrorを発生させるという問題が報告されました(#57264)。deliver_all_laterはドキュメントに記載された公式APIにもかかわらず、新規Railsアプリケーションでは利用できない状態でした。
原因はオートロードの順序にありました。deliver_all_laterとperform_all_laterの両メソッドは、それぞれactionmailer/lib/action_mailer/message_delivery.rbとactivejob/lib/active_job/enqueuing.rbに定義されていました。これらのファイルはautoloadによって遅延ロードされるため、MessageDeliveryやEnqueuingが実際に参照されるまでメソッドがActionMailerモジュールおよびActiveJobモジュールに定義されません。アプリケーション起動直後にActionMailer.deliver_all_laterを呼ぶと、message_delivery.rbがまだロードされておらずメソッドが未定義となっていました。
Workaroundとしてconfig/application.rbでrequire 'action_mailer/delivery_methods'を手動で記述する方法が存在しましたが、公式APIがデフォルトで動作しないことは明らかな設計上の問題でした。
技術的な変更
メソッド定義を、遅延ロードされるファイルからモジュールのエントリポイントへ移動することで、起動時に確実にロードされるようにしました。コードの内容自体に変更はありません。
ActionMailer.deliver_all_later / deliver_all_later!の移動:
actionmailer/lib/action_mailer/message_delivery.rbから削除され、actionmailer/lib/action_mailer.rb内のclass << selfブロックに移動しました。action_mailer.rbはRailsの起動プロセスで必ず読み込まれるエントリポイントであるため、MessageDeliveryのオートロードを待たずにメソッドが定義されます。
class << self
def deliver_all_later(*deliveries, **options)
_deliver_all_later("deliver_now", *deliveries, **options)
end
def deliver_all_later!(*deliveries, **options)
_deliver_all_later("deliver_now!", *deliveries, **options)
end
def eager_load!
super
require "mail"
Mail.eager_autoload!
Base.descendants.each do |mailer|
mailer.eager_load! unless mailer.abstract?
end
end
private
def _deliver_all_later(delivery_method, *deliveries, **options)
# ...
end
end
ActiveJob.perform_all_laterの移動:
同様に、activejob/lib/active_job/enqueuing.rbからactivejob/lib/active_job.rbへ移動しました。enqueuing.rbはActiveJob::Baseがロードされて初めて読み込まれるファイルであり、ActiveJob.perform_all_laterを使うだけならBaseのロードは不要なはずです。エントリポイントへの移動により、ActiveJobモジュール自体がロードされた時点でメソッドが利用可能になります。
なお、eager_load!メソッドも同じclass << selfブロックに収まるよう整理されており、action_mailer.rb全体の構造がより凝集度の高いものになっています。
設計判断
コードを変えずに配置だけを変えるという最小限のアプローチが選択されました。PR本文にも「This PR only moves some code around, without any code changes.」と明示されています。
Rubyのオートロードは「そのクラス・モジュールが初めて参照されたときにファイルをロードする」仕組みです。モジュールレベルのメソッドをオートロード対象のファイルに置くと、そのファイルが参照されるまでメソッドが存在しないという問題が生じます。今回の修正はこの原則に従い、「常に利用可能であるべきメソッドはエントリポイントに置く」という明確な方針を示しています。
別の修正案として、autoloadの代わりにrequireを使う方法や、const_missingでのフォールバックなども考えられますが、それらはより大きな変更を伴います。ファイルの移動だけで解決できる今回のケースでは、影響範囲を最小化したアプローチが合理的な選択といえます。
まとめ
deliver_all_laterとperform_all_laterをオートロード対象ファイルからモジュールのエントリポイントへ移動するだけで、Railsアプリケーションの起動直後から両メソッドが確実に利用可能になりました。「公開APIは常にアクセス可能であるべき」という原則を、コードの変更なしに配置の是正だけで実現した、シンプルかつ明快なバグ修正です。