Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Railsコールバックの落とし穴:after_saveからafter_commitへ(Rails 5での修正)

ActiveRecordのコールバックは、モデルのライフサイクルのさまざまな段階でコードを実行できる手軽な仕組みです。

たとえば、Q&Aサイトを運営していて、すべての質問を検索できるようにしたいとしましょう。質問に変更を加えるたびに、ElasticSearchのような検索エンジンへインデックスを登録する必要があります。インデックス処理には時間がかかり、緊急性もないため、Sidekiqを使ってバックグラウンドで実行することになるでしょう。

これはafter_saveコールバックを使う絶好の場面に思えます!そこで、モデルに次のように書くはずです:

app/models/question.rb
class Question < ActiveRecord::Base
  after_save :index_for_search

  # ...

  private

  def index_for_search
    QuestionIndexerJob.perform_later(self)
  end
end
app/jobs/question_indexer_job.rb
class QuestionIndexerJob < ActiveJob::Base
  queue_as :default

  def perform(question)
    # ... 質問をインデックスに登録する ...
  end
end

これでうまく動きます!少なくとも、そう見えます。しかし、大量のジョブをキューに投入するようになると、次のようなエラーが現れ始めます:

2015-03-10T05:29:02.881Z 52530 TID-oupf889w4 WARN: Error while trying to deserialize arguments: Couldn't find Question with 'id'=3

確かにSidekiqはジョブをリトライするので、次回はおそらく成功するでしょう。それでも少し奇妙です。なぜSidekiqは、保存したばかりの質問を見つけられないのでしょうか?

プロセス間のレースコンディション

Railsは、レコードが保存された直後にafter_saveコールバックを呼び出します。しかし、そのレコードは、データベースのトランザクションがコミットされるまで、Sidekiqが使っているような他のデータベース接続からは見えません。コミットはその少し後に行われます。つまり、Sidekiqが質問を探すタイミングが「保存後・コミット前」になってしまう可能性があるのです。レコードが見つからず、ジョブはクラッシュします。

この問題はあまりにもよくあるため、SidekiqにはFAQエントリまで存在します。そして、解決策はとてもシンプルです。

after_saveの代わりに:

app/models/question.rb
class Question < ActiveRecord::Base
  after_save :index_for_search

  # ...
end

after_commitを使いましょう:

app/models/question.rb
class Question < ActiveRecord::Base
  after_commit :index_for_search

  # ...
end

こうすれば、Sidekiqからモデルが見えるようになるまで、ジョブはキューに入りません。

要するに、バックグラウンドジョブをキューに投入するときや、他のプロセスに変更を通知するときは、after_commitを使うのが鉄則です。そうしないと、相手側が直前に操作したレコードを見つけられないことがあります。

でも、もうひとつ問題が…

さて、after_saveフックをいくつもafter_commitに置き換えました。すべてうまく動いているようです。ここでコミットして帰宅する準備は万端ですね?

まず、テストを実行してみましょう:

test/models/question_test.rb
require 'test_helper'

class QuestionTest < ActiveSupport::TestCase
  test "A saved question is queued for indexing" do
    assert_enqueued_with(job: QuestionIndexerJob) do
      Question.create(title: "Is it legal to kill a zombie?")
    end
  end
end
  1) Failure:
QuestionTest#test_A_saved_question_is_queued_for_indexing [/Users/jweiss/Source/testapps/after_commit/test/models/question_test.rb:7]:
No enqueued job found with {:job=>QuestionIndexerJob}

あれ?テストでジョブがキューに入るはずでは?いったい何が起きたのでしょう?

デフォルトでは、Railsは各テストケースを独自のデータベーストランザクションでラップします。これによりテストが大幅に高速化されます。テスト中に行った変更すべてを、たった1つのデータベースコマンドでロールバックできるからです。

しかしこれは、after_commitコールバックが実行されないことも意味します。after_commitコールバックは、最も外側のトランザクションがコミットされたときにのみ実行されるからです。

テストケース内でsaveを呼ぶと、そこでもトランザクションがコミットされます(多かれ少なかれ)が、それは今や外から2番目のトランザクションです。そのため、after_commitコールバックは期待したタイミングで実行されず、その内部で何が起こるのかをテストできません。

この問題にも簡単な解決策があります。Gemfileにtest_after_commit gemを追加してください:

Gemfile
group :test do
  gem "test_after_commit"
end

すると、after_commitフックは、最後から2番目のトランザクションがコミットされた後に実行されるようになります。これこそ、あなたが期待していた動作です。

「おかしいな。Railsに標準で付属しているコールバックをテストするために、わざわざ別のgemを導入しなければならないなんて。自動的に動くべきではないのか?」と思うかもしれません。

その通りです。確かに奇妙です。でも、この奇妙さは長くは続きません。

Rails 5がリリースされれば、test_after_commitについて心配する必要はなくなります。この問題は約1ヶ月前にRails本体で修正されたからです。

まとめ

私自身のコードでは、after_commitを頻繁に使っています。after_saveよりも使っているくらいです!しかし、問題や奇妙なエッジケースに遭遇しなかったわけではありません。

とはいえ、バージョンを重ねるごとに状況は改善されています。適切な場面でafter_commitを使うようにすれば、奇妙で断続的な例外の多くは二度と発生しなくなるでしょう。

  1. Vue、Vuex、Railsを使用したフルスタックアプリケーションの構築

    スケーラビリティを念頭に置いてフルスタックアプリケーションを構築することは、特に、完全なタイプスクリプトをサポートする最新バージョンのVueおよびVuexを使用して構築する場合、威圧的になる可能性があります。この記事では、不健康な家畜への治療の処方を管理するCRUDアプリケーションを探索することで、APIリクエストとデータベースの相互作用を処理するVuex4.0を使用した状態管理からスケーラブルなフルスタックアプリケーションを構築するために知っておく必要のあるすべてを読者に教えます。バックエンドはRailsで構築され、フロントエンドによる統合のために基本的なCRUDAPIを公開します。 ほと

  2. Chrome と Edge で RESULT_CODE_HUNG を修正

    複数のブラウザーがインターネット ドメインの拠点を占めていますが、Google Chrome と Microsoft Edge はそのリストの中で際立っています。 Chrome は世界中の何百万人ものユーザーにとって頼りになる選択肢ですが、私の数人の Windows ユーザーには Edge が好まれています。しかし、これらの優れたブラウザーにもいくつかの欠陥があります。ユーザーは、インターネット サーフィン中にいくつかのよくあるエラーに気を取られることがよくあります。そのようなよくあるエラーの 1 つに Aw Snap! RESULT_CODE_HUNG . Chrome、Edge、Brave