Saga
In a distributed system, sometimes you need a concept similar to a transaction in a database. That is, several operations spanning different microservices must succeed or fail as a unit; otherwise, we may end up in an inconsistent state. A saga implements this concept by providing for each action a corresponding compensating action, which is executed if any of the following steps fail. The role of the compensating action is to undo any changes performed by the action, hence taking the system to the state before the entire operation beginning its execution.
Saga distributed transactions pattern in Cloud Design Patterns.
Arrow Resilience provides the saga
function, which creates a new scope where compensating actions can be declared
alongside the action to perform. This is done by the saga
function in
SagaScope
.
The resulting Saga<A>
doesn't perform any actions, though; you need to call
transact
to keep the chain going.
Let's use a small counter as an example, which we implement using the
Atomic
type provided
by Arrow.
import arrow.atomic.AtomicInt
import arrow.resilience.*
val INITIAL_VALUE = 1
object Counter {
val value = AtomicInt(INITIAL_VALUE)
fun increment() {
value.incrementAndGet()
}
fun decrement() {
value.decrementAndGet()
}
}
Now we create a saga with a couple of operations. The first one increments the counter, so the compensating action must be decrementing it. The second action simply fails; we include no compensation because we know that part is never reached.
val PROBLEM = Throwable("problem detected!")
// describe the transaction
val transaction: Saga<Int> = saga {
saga({
// action to perform
Counter.increment()
}) {
// inverse action for rolling back
Counter.decrement()
}
saga({
throw PROBLEM
}) {}
// final value of the saga
Counter.value.get()
}
Executing the transaction gives the expected results:
- The exception raised in the second step bubbles up to the caller of
transact
. In this case, we useEither.catch
to turn it intoEither
. - The counter has been correctly decremented as part of the compensation process in the saga.
suspend fun example() {
// perform the transaction
val result = Either.catch { transaction.transact() }
result shouldBe PROBLEM.left()
Counter.value.get() shouldBe INITIAL_VALUE
}
SagaScope
has many parallels with ResourceScope
: both ensure that some
operations are performed at a certain point, and saga
and install
require
an action that "undoes" something. The main difference is that ResourceScope
always runs the release actions, whereas SagaScope
only runs compensation
if the entire action fails.
If you need to perform several actions as a unit over local data, Software Transactional Memory is a better tool than Sagas and Atomic references.