ワシはワシが育てる

週刊少年ジャンプと任天堂のゲームが三度のメシより好きです。

sidekiqで非同期に通知を送るときにハマったこととその対処法

Railsの非同期処理で最も人気のあるライブラリの一つであるSidekiqでハマったこととその対処法について。

とあるテーブルのレコードが保存されたタイミング(after_createなど)でobserverがそれを拾い、sidekiqに通知タスクを投げた際、sidekiqがそんなレコード存在しないよとエラーが出してしまいました。

createしてるんだから存在しないはずないやろと最初は思いましたが、after_createの段階ではデータベースのコミットが完了しておらず、sidekiqの引数に渡されたIDの実体がまだ存在していなかったようです。

(※sidekiqではキューに入ったタスクが実行されるまでにレコードが変更される可能性があるため、レコードの実体ではなくIDを引数に渡すことを推奨している)

ではコミット後にタスクを投げれば良いのでafter_createをafter_commitに入れ替えて完了、とはいきません。
after_commitはcreateもupdateもdestroyも拾ってしまうので、それぞれを判別する必要があります。

テーブルごとにコールバックを記述していれば

after_commit :hogehoge, on: :create

というように記述すればよいのですが、テーブル構成が玉石混交の通知タスクの場合、どうしてもobserverにまとめておきたかったので、上のような記述ができません。

(※こちらのブログではレコードのsendメソッドを用いることで解決できるとしていますが、当方の環境では空を返してしまい動作しませんでした)

ということであまりクリーンな方法ではありませんが、createとupdateの判別として単純に時刻で比較することにしました。

class HogeObserver < ActiveRecord::Observer
  observer :hoge_table

  def after_commit(record)
    if record.created_at == record.updated_at 
      # ここをcreateとみなす
    else
      # ここをupdateとみなす
    end
  end
end

またupdateの場合、とあるカラムが変更されたかどうかをcolumn_chaged?で判別できましたが、コミット後は利用できません。
ということでprevious_changesを使用します。例えばemailが変更されたと検知するには以下のように記述します。

record.previous_changes.include?(:email)

かなり場当たり的な対応であることは否めませんが、一旦は以上の対応で正常に動作しています。

※正規の方法を後から見つけたので、こちらも参考にして下さい