この記事は裏freee developers Advent Calendar 2018の16日目の記事です。 裏感なく普通にtips的な内容です。
ありがち?なバグ
かなり大雑把に表現すると↓のようなコードがありました。 do_something1
で新しく Model を作ったり更新したりしたあと、最終的な状態に基づいてメールを送信するという具合です(記事中の Model という言葉は ActiveRecord で表現される app/models
以下のやつらを指します)。 send_email
は内部で ResqueMailer に処理を投げています。 do_something1
の部分で操作した Model をidベースで Mailer に渡し、Mailer 側ではidを使って取得するようになっています。
1 | class Hoge |
do_something2
の処理が完了する前(=トランザクションがコミットされる前)に Mailer の処理が動いてしまうと、古い状態の Model が見えてしまったり、存在しない Model を取得しようとしてエラーになってしまいます。
ちょいちょい発生していたエラーを追っていたら、以上のような構造になっていました。
シュっと対応
トランザクションがコミットされる前に Resque 経由で Mailer が動いてしまうことが問題なので Resque に積むタイミングをトランザクションがコミットされた後にすれば解決します。(この例の場合なら do_something2
の位置をずらせばほぼ解決ですが)
1 | class Hoge |
めでたしめでたし……?
ダメじゃん
これではダメです。 次のように ActiveRecord::Base.transaction
がネストしているケースで問題が発生します。
1 | class Hoge |
実際のDBのトランザクションは Fuga
側のブロックの終わりにコミットされるため、トランザクションの外に追いやったはずの send_email
がトランザクションの中残ったままになってしまっています。
after_commit
が使えれば良いのですが、 after_commit
は ActiveRecord のコールバックなので Model 以外では使えません。 Perl を書いていたときに使った DBIx::TransactionManager::EndHook を思い出し、それっぽいのものがないのかを探しました。
after_commit_everywhere
この記事を見つけ、そのコメント中で宣伝されていた after_commit_everywhere という Gem がインタフェース的にまさに欲しかったものでした。 中身を読んでみたところかなりコンパクトで、いざとなったらまるまるメンテできそうで安心したので採用しました。
after_commit_everywhere
を使用して書き直すと以下のようになります。
1 | class Hoge |
これで Hoge
が意図せずトランザクションの中で実行されたとしても send_email
はコミットされた後に実行され、 Mailer が処理を行う時点で最新の Model を取得できるようになりました。
終わりに
多くのケースでは ActiveRecord のコールバックを普通に使えば十分ですが、たまに ActiveRecord ではない部分でもコミットをトリガーとして処理を実行したくなるかと思います。 使いすぎると可読性がヤバいことになりそうなのでご利用は計画的に。
明日17日はyo_wakaによる新作発表らしいです。お楽しみに。