やりたかったこと
- 特定attributeの更新があった場合に、外部サービスと連携したい
- 外部サービスの操作はワーカープロセスに任せて、非同期でデータ更新
概要&サンプルコード
- 特定attributeが更新された場合、Sidekiq::Workerにenqueue
i. after_saveにて、attribute_changed?で更新確認
ii. 更新されていればenqueue - Worker側で更新対象取得
- 外部サービス情報取得、更新
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# external_user_id :integer
# external_user_name :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
after_save : refresh_external_user_name
def update_external_user_name
client = ExternalService::Client.new
name = client.get_name(external_user_id)
update(external_user_name: name)
end
private
def refresh_external_user_name
if external_user_id_changed?
ExternalServiceWorker.perform_async self.id
end
end
end
class ExternalServiceWorker
include Sidekiq::Worker
def perform(id)
user = User.find(id)
user.update_external_user_name
end
end
発生した問題
- Worker内で行っている、User.find(id)がRecordNotFoundとなる
- 成功するケースもある
- RecordNotFoundがでた後に、idでDBを確認するとデータは存在してる
原因
after_saveの時点でidは発行されているため、データが存在しているようにみえた
しかし実際にはTransactionはCOMMIT前なので、DBにデータはまだ存在していない
そのためWorker内のUser.find(id)が、COMMITより先に走ってしまった場合RecordNotFoundとなる
COMMITが完了していれば、DBにもデータが存在するので成功する
解決方法
- 確実にTransactionがCOMMITされてるタイミング(after_commit)で、enqueueするようにした
- after_commit内ではattribute_changed?メソッドを使えないため、previous_changes.has_key?(:external_user_id)による更新確認に変更
コード変更点
- after_save :refresh_external_user_name
+ after_commit :refresh_external_user_name
def refresh_external_user_name
- if external_user_id_changed?
+ if previous_changes.has_key?(:external_user_id)
ExternalServiceWorker.perform_async self.id
end
end
教訓
Callback利用して特定の値が更新された時にjob登録、別プロセスでデータ追加更新みたいなことをしたいケースではTransactionの確定タイミングに注意してプログラムを組むのが大事。