【Rails】after_createが発動するタイミングはいつでしょう?
MUGENUPの倉成です。
今回はRailsのCallbackであるafter_create
とafter_commit
の処理順番を改めて確認し、処理順番を誤解していた事によって僕が遭遇した問題ついて記事を書こうと思います。
シチュエーション
ブログ記事の新規投稿があった時、購読者に対してメールを送信する。
ブログの投稿時に通知のメールを送信するような場合、記事の投稿に必要な最低限の処理のみを行い、購読者へのメール送信などリアルタイムな処理が必要でないものは非同期で処理することで、レスポンス速度を向上させることが出来ます。
非同期処理を行うためのライブラリとしてはresqueやsidekiq、delayed_jobなど幾つかのものがありますが、弊社ではこの用途にresqueを使っているので、この記事では特にresqueを取り上げて説明します*1。
実装
さて、上の要件の実装として、僕は以下のようなコードを書きました。
# app/models/article.rb class Article < ActiveRecord::Base # 記事が作成されたらメール送信タスクをqueueに積む after_create do Resque.enqueue(SendMail, id) end end
# app/workers/send_mail.rb class SendMail @queue = :send_mail def self.perform(article_id) # 投稿された記事をDBから読み込む article = Article.find(article_id) # ブログの購読者にメールを送信 # .... # .... end end
self.perform(article_id)
の第一引数article_id
はResque.enqueue(SendMail, id)
の第二引数id
と対応しており、
新規記事のIDが渡されることになります。
さて、実際にアプリケーションを立ち上げて実行してみるとself.perform
内部で
article = Article.find(article_id)で稀にNoRecordFoundが発生する
という現象が起きていました。
after_create
という名のとおり記事は既にcreate
されていて、レコードは存在しているはずです。その証拠に既にidは与えられていますよね。
なぜでしょう。
after_createはDBへのコミット直前に実行される
不思議に思い、出力されたログを見てみると、ResqueからのSELECT
はBEGIN ~ COMMIT
の内部で実行されていました。
DBはINSERT
を実行しても、COMMIT
されるまでは、レコードは永続化されません。
つまりRailsがDBにCOMMIT
する前に、外部プロセスであるResque
のプロセスがSELECT
をしていたため、NoRecordFound
が発生していまうという状態になっていました。
BEGIN INSERT INTO `articles` (`col1`, `col2`) VALUES ('..', '..') /* COMMIT前にResqueからのSELECTが発行される */ SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1 COMMIT
これは、after_create
がDBへのコミット直前
に実行されることが原因です*2。
after_commitを使う
原因がわかりました。
after_create
の代わりに、コミット直後
に実行されるafter_commit on: :create
を使いましょう。
これでCOMMIT
のあとにResqueの処理が実行されるため、NoRecordFound
が発生することもなくなりました。
BEGIN INSERT INTO `articles` (`col1`, `col2`) VALUES ('..', '..') COMMIT /* COMMIT後にResqueからのSELECTが発行される */ SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1
BEGIN ~ COMMIT
の外側でSELECT
が実行されていることが、ログでも確認できます。
NoRecordFoundの原因は
稀にNoRecordFoundが発生する状態だったのは「COMMIT
の前に外部プロセスであるResqueのSELECT
が発生するケースがあったため」というのが結論です。なお常に発生する
では無いのは、Resqueが新規ジョブをポーリングする間隔により、Resqueの処理がCOMMIT
後に行われることもあったためです。
今回のようにRailsのcallback
をトリガーにして外部プロセスからDBにアクセスする場合はafter_commit
を使うようにしましょう。
なお,同様の注意点は http://apidock.com/rails/ActiveRecord/Callbacks/after_createにも書いてありました*3。
おわりに
今回は「after_create
はDBにCOMMIT
された後に呼び出される」という間違った認識によって遭遇した問題について記事を書かせていただきました。
ResqueなどRails外のプロセスを使う場合にこの記事を思い出していただけたら幸いです。
*1:今回は説明のため使用しませんがメール送信を非同期で行うならresque_mailerが便利です。
*2:コールバックの順番はこちらの記事にとても詳しくまとまっています http://techracho.bpsinc.jp/baba/2013_08_23/12670