`ActiveJob::Attributes` でマルチステップジョブのデータをステップ間に引き継ぐ
Active JobにActiveJob::Attributesモジュールが追加され、ジョブのシリアライズ・デシリアライズをまたいで型付きの属性値を保持できるようになりました。ActiveJob::Continuableとの組み合わせで、中断・再開を繰り返すマルチステップジョブのステップ間データ受け渡しが宣言的に記述できます。
背景
マルチステップジョブにおいて、あるステップの結果を後続ステップで利用したい場合、これまではserialize/deserializeを手動でオーバーライドするしか手段がありませんでした。Rails 8.1でActiveJob::Continuableがマルチステップジョブを第一級の機能として取り込みましたが、ステップ間でデータを引き継ぐ仕組みは提供されていませんでした。
PRの作者は、この問題をjob-iterationとserialize/deserializeのオーバーライドで解決していたと述べています。ActiveJob::Continuableのstep DSLはエルゴノミクスを改善しましたが、中断・再開をまたいだデータ永続化の手段が欠けていたため、本PRがその能力を追加します。
この変更はRails Discussフォーラムでの提案を経て実装されており、API設計についてもstepの戻り値を保存する方式と属性宣言方式の比較検討が行われました。
技術的な変更
ActiveJob::Attributesモジュールが新たに追加され、ActiveModel::AttributesのAPIをActive Jobのシリアライズ機構と統合しています。ActiveJob::Continuableはデフォルトでこのモジュールをincludeするよう変更されました。
activejob/lib/active_job/attributes.rb(新規追加)はActiveModel::Attributesをincludeし、serializeとdeserializeをオーバーライドすることで属性値の自動的な保存と復元を実現します。
module ActiveJob
module Attributes
extend ActiveSupport::Concern
include ActiveModel::Attributes
def serialize # :nodoc:
super.merge("attributes" => serialize_attribute_values)
ジョブのシリアライズ時に"attributes"キー配下へ属性値が書き出され、デシリアライズ時に復元されます。属性値はActive Job Argumentsとして扱われるため、Active Jobがサポートするすべての型(プリミティブ型、GlobalIDを実装したActive Recordオブジェクト等)が利用可能です。
ActiveJob::Continuableへの統合は1行の追加で完結しています。
module ActiveJob
module Continuable
extend ActiveSupport::Concern
+ include ActiveJob::Attributes
実際の利用は以下のように記述します。attributeマクロで型と任意のデフォルト値を宣言し、ステップ内でself.属性名 =で値を設定するだけです。
class SubmitEnrollmentJob < ApplicationJob
include ActiveJob::Continuable
attribute :payment_token, :string
attribute :billing_profile_id, :integer
def perform(enrollment)
step(:tokenize_payment_instrument) do
self.payment_token = PaymentGateway.tokenize(enrollment.user.payment_instrument)
end
step(:create_billing_profile) do
self.billing_profile_id = BillingProfileApi.create(customer_id: enrollment.user_id)
end
step(:submit_enrollment) do
submission_id = EnrollmentApi.submit(enrollment, billing_profile_id)
enrollment.update!(status: 'processing', submission_id: submission_id)
end
end
end
また、activemodel/lib/active_model/attributes.rbのinitializeメソッドのシグネチャが*から...に変更されています。これはキーワード引数やブロックも含めてサブクラスへ転送できるようにするための修正で、ActiveJob::Attributesとの統合においてsuperの引数転送を正しく機能させるための対応です。
ActiveJob::AttributesはActiveJob::Continuableなしでも単独でinclude可能で、その場合はリトライをまたいで属性値が保持されます。
設計判断
Active Model Attributesを再利用する設計が採用されました。型キャスト、デフォルト値、カスタム型の仕組みをすべてActive Modelから継承することで、新たな型システムを設計・実装することなく、既存の豊富な型サポートをそのまま利用できます。
PR内では、stepの戻り値をオプションで保存するAPI(例: step(:tokenize, store: true))も検討されましたが、採用されませんでした。PRの作者は「属性宣言の方がシンプルかつ明示的であり、カーソルをstep.cursorやset!/advance!で明示的に扱う既存のContinuable APIとの一貫性がある」と述べています。
カスタム型を利用する場合、属性値はActive Job Argumentsとしてシリアライズ可能である必要があります。これは既存のActiveJob::Argumentsの制約をそのまま継承する設計であり、シリアライズ機構を二重に持たずに済むというトレードオフの選択です。
まとめ
ActiveJob::Attributesは、Active ModelのAPIをActive Jobのシリアライズ機構へ接続するという最小限の実装で、マルチステップジョブにおける課題を解消しています。serialize/deserializeの手動オーバーライドという定型的なパターンをattributeマクロに置き換えることで、ActiveJob::ContinuableのDSLと自然に馴染む宣言的なインターフェースが実現されました。