3
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

トランザクションをコミットする前に非同期処理を実行しないほうがいいケース

説明用に、主にRailsを使っている場合を例として取りあげます。次のコード

Task.transaction do
  task = Task.create(task_params)
  NotificationJob.perform_later(task)
end

は、ときどき以下のようなエラーを発生させることがあります。

ActiveJob::DeserializationError: Error while trying to deserialize arguments: Couldn't find Task with 'id'=1

DBからデータをオブジェクトとしてデシリアライズしようとして失敗しています。この原因を説明します。

まず、RDBではデフォルトのトランザクション分離レベルがread committedかそれより厳密であることが多いです1。read committedの場合、あるトランザクションでコミットされたデータは別のトランザクションからも読み取ることができます。

また、Task のレコードは Task.transaction のブロックを抜けるまではコミット済みになりません。このとき、トランザクション内でActive Jobの perform_later を実行すると、キューにジョブが入り、ワーカーがキューからジョブを取り出して処理します。

ワーカーがジョブを取り出すタイミングによっては、Task のレコードを保存するトランザクションがコミットされる前に非同期処理が実行されることがありえます。これが起きると、非同期処理はデータを保存しようといているトランザクションとは別のトランザクションとなり、また、分離レベルがread committed以上であることから、まだDBから読み取ることができないデータをしばしばオブジェクトとしてデシリアライズしようとし、結果的にデータが見つからずに失敗となります。

次のような処理の構造にすると、この問題を回避できます。

# トランザクション内ではレコードの保存だけ実行する
task = Task.transaction do
  Task.create(task_params)
end

# 確実にコミット済みのデータを非同期処理の中で読み取る
NotificationJob.perform_later(task)

  1. PostgreSQLではread committed、MySQL (InnoDB) では、もう一段階厳密な分離レベルであるrepeatable readがデフォルト 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?