読者です 読者をやめる 読者になる 読者になる

The transaction is not active!

Transaction is not active: javax.transaction.RollbackException: The transaction is not active!

というエラーと奮闘した話(JBoss AS5.1.0GA/Seam2.2/EJB3

メッセージでそのままぐぐればTech Team Lead Newsの記事が見つかるので、トランザクションタイムアウトの値を増やしてあげればよい、という話。

…なんだけど、結局トランザクションタイムアウトを増やしても、どれくらいの値が最適なのか、とか、確率が減るだけで起こる可能性はなくなってない、といった問題は残ったまま。そんなわけで、エラーをもう少し詳しく調べることにした。

よりきれいな対処のためにはJBoss、というか、Java EEトランザクション属性についての理解が必要だったので、ここでまとめておく(以前にも似たようなことでハマった記憶があるけど)。トランザクション属性についての説明を見れば、それぞれの意味はわかるけど、どう使い分ければよいのかの指針が完全に抜け落ちていたなぁ。

個人的な感想としては、各トランザクション属性は、そもそもそのトランザクションを使うのかどうか、呼び出し元のトランザクションが開始されている時にそのトランザクションが中断されるのかどうか、という観点を意識して選択する必要があると感じた。

トランザクション属性 トランザクションを使う? 呼び出し元のトランザクションは中断?
(呼び出し元のトランザクションが開始されている時)
MANDATORY ×
REQUIRED ×
REQUIRES_NEW
SUPPORTS ×
NOT_SUPPORTED ×
NEVER × ×


以上を元に、トランザクションタイムアウトに話を戻すと、呼び出し元のトランザクションが中断しても、中断している間もトランザクションタイムアウトの計算時間はカウントされている、という点が重要。

例として以下のケースを考えてみる。

  • トランザクションタイムアウトは10秒
  • REQUIRED宣言されているBeanのメソッドAが、内部で、REQUIRES_NEW宣言されているBeanのメソッドB、メソッドC、メソッドDを順に呼び出す。

メソッドBとメソッドCにそれぞれ5秒ずつかかった場合、メソッドDが呼ばれた時(正確には、メソッドCを抜けた後のメソッド)に、トランザクションタイムエラーが起こる。ここが勘違いしていたポイントその1で、REQUIRES_NEWで新しくトランザクションを開始しているし、そもそも「中断」なんだから、メソッドAのトランザクションタイムアウトはメソッドBの実行時間が除外されて計算されると思っていた。

また、トランザクションタイムアウトエラーの発生ポイントは、タイムアウト時間を超過してすぐではなく、その次のトランザクション境界でトランザクションを使う時。今回の例では、メソッドC実行時に10秒のタイムアウトを超えるので、メソッドC実行中にエラーが起こるものだと勘違いしていたけど、実際はそうではない。

 この辺は、もしかするとこれはJBoss ASの実装に依存した話かもしれない…。

解決

というわけでどうすればいいかというと、メソッドAを呼び出すBeanのトランザクション属性をNOT_SUPPORTEDにした。こうすると、メソッドAはトランザクションを開始しないため、トランザクションタイムアウトは起きない。メソッドB、メソッドC、メソッドDがそれぞれ10秒以内に終わりさえすれば、トランザクションタイムアウトは起きずに処理が完了する。

まとめ

というわけでまとめ。ざっくりとこんな感じの指針でトランザクション属性を使うことにした。要約すると、「トランザクション境界をできる限り狭くして、トランザクションタイムアウトが起こる可能性を減らす」ってことになる。

  • データの登録・更新・削除を行うBean
    • 呼び出し元を含めて一つのトランザクションにまとめたい⇒REQUIRED(デフォルト)
    • 呼び出し元ではないけど、複数のメソッドをまとめて一つのトランザクションにまとめたい⇒SUPPORTS
    • 呼び出し元の結果によらずとにかくコミットさせたい⇒REQUIRES_NEW
  • データの登録・更新・削除を行わないBean⇒NOT_SUPPORTED

重要なのは、Beanの設計として、検索しか行わないトランザクション不要な処理で構成されるBeanと、登録・更新・削除を行うトランザクションが必要なBeanをきちんと分けること。