[cats-saga] 補償処理中に例外が発生した場合の挙動

cats-sagaの補償処理中に例外が発生した場合、後続の補償処理は実行されません。また発生した例外は送出されないため、補償処理中で適切に処理する必要があります。
2020.12.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

前回紹介したcats-sagaで補償処理中に例外が発生した場合はどうなるの?という疑問が沸いたので動作確認してみました。

ネタバレ

実は公式ドキュメントに記載があります。

By default, if some compensation action fails no other compensation would run and therefore user has the ability to choose what to do: stop compensation (by default), retry failed compensation step until it succeeds or proceed to next compensation steps ignoring the failure.

やってみる

とはいえやってみます。

package example

import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._

object CompensateFailed extends IOApp {

  import com.vladkopanev.cats.saga.Saga._

  override def run(args: List[String]): IO[ExitCode] = (for {
    _ <- IO.unit.compensate(IO(println("first compensate")))
    _ <- IO.raiseError[Unit](new RuntimeException("Error on second process")).compensate(
      IO(println("executing second compensate")) *> IO.raiseError(new RuntimeException("Error on compensate"))
    )
  } yield ExitCode.Success).transact
}

実行結果は以下の通りです。1つ目の補償処理しか実行されません。まぁそうですよね・・・。

executing second compensate
java.lang.RuntimeException: Error on second process
	at example.CompensateFailed$.$anonfun$run$2(CompensateFailed.scala:13)
	at com.vladkopanev.cats.saga.Saga.$anonfun$flatMap$1(Saga.scala:42)
	at com.vladkopanev.cats.saga.Saga.$anonfun$transact$5(Saga.scala:79)
	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:142)
	at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:359)
	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:380)
	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:323)
	at cats.effect.internals.IOShift$Tick.run(IOShift.scala:35)
	at cats.effect.internals.PoolUtils$$anon$2$$anon$3.run(PoolUtils.scala:52)

エラーの場合どうするか

では補償処理中に発生した例外はどう扱うべきでしょうか。以下のような追加の対応が考えられます。

  • エラーの内容をログに記録する
    • 最低限これはやっておきたいです
  • 外部APIの一時的なエラー(ビジーなど)の場合には非同期での補償処理に切り替える
    • Akka actorやSQS+ワーカーを使ったパターンで補償処理をリトライします
  • その場でリトライする
    • Sagaはcats-retryによるリトライをサポートしています。自前で実装したリトライ処理込みのIOを補償処理として渡すこともできます(バックオフ制御を再実装するよりはcats-retryを使った方がいいとは思います)

まとめ

cats-sagaでは補償処理中に発生した例外は明示的に処理する必要があります。