結論
ハマったら .as_json で渡しましょう。
*** ActiveJob は GlobalID に対応しているので以下の心配は不要です**
理由
class User < ActiveRecord::Base
before_save :notify
def before_save
Notifier.delay.push(self)
end
end
これは期待通りに動かない (ことがある)。
delay は巧妙な構文で仕組みを隠蔽しているが、実際に内部で起きていることは 'Notifier' と 'push' と 'self' を全部ひとまとめにした yaml 文字列を作って Delayed::Job のインスタンスを生成しているだけだ。
分かりやすく書けばこんな感じ。
Delayed::Job.new(PerformableMethod.new(Notifier, :push, [self]))
PerformableMethod は渡された3つのパラメタを纏めて1つのyaml文字列を生成する。これが delayed_jobs テーブルに保存される。
一方、別プロセスで動作している delayed_job デーモンは、このyaml文字列から元のオブジェクトを復元する。
これらの一連の変換は
https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/psych_ext.rb
に記述されている。
さて、save 前の ActiveRecord インスタンスはどのように delayed_job デーモンに渡るのか。実際に delayed_jobs テーブルの実行前のレコードを見てみると、各カラムの値が記録されているのがわかる。
しかし油断してはいけない。先ほどの psych_ext.rb の、 yaml からインスタンスを復元する部分のコードを確認してみよう。
case object.tag
when /^!ruby\/ActiveRecord:(.+)$/
klass = resolve_class(Regexp.last_match[1])
payload = Hash[*object.children.map { |c| accept c }]
id = payload['attributes'][klass.primary_key]
begin
klass.unscoped.find(id)
rescue ActiveRecord::RecordNotFound => error
raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
end
この通り、 klass.unscoped.find(id)
となっていて、 idしか見ていない ことがわかる。
user が new インスタンスで id が振られてなければ、 RecordNotFound になってしまう。
あるいは id を既に持っていても、この job が実行される時点でもしまだレコードが保存されてなければ、 notify メソッドは保存前のレコードの内容で実行されることになる。
あるいは、 delayed_job の実行が十分に遅れて、その間に DB の内容が変わっていたら、最新のデータで notify が呼ばれることになる。
レシーバが ActiveRecord インスタンスの場合も同様の問題が起きる。
def before_save
self.delay.notify
end
せっかく全カラムの情報渡してるんだから、その通りに復元してくれればいいのにと思いました。
余談
DelayedJob 公式には、 Delayed::Job レコードの id を取得する方法が用意されてないので実行結果の追跡が困難なのだが、実は
job = Notifier.delay.push(self)
とすると Delayed::Job インスタンスが取得できるので job.id
で id が簡単に取れる。