defmodule Saga do
def run(steps) do
result =
Enum.reduce_while(steps, [], fn {name, action, compensate}, done ->
case action.() do
{:ok, result} ->
IO.puts(" #{name}: ok")
{:cont, [{name, compensate, result} | done]}
{:error, reason} ->
IO.puts(" #{name}: failed (#{inspect(reason)})")
{:halt, {:failed, name, reason, done}}
end
end)
case result do
{:failed, name, reason, done} ->
# `done` is newest first: exactly the order to undo.
Enum.each(done, &compensate/1)
{:error, name, reason}
_done ->
:ok
end
end
# A compensation must not be skipped: a half-cancelled
# trip is the thing the saga exists to prevent. So it's
# retried until it works.
defp compensate({name, compensate, result} = step) do
case compensate.(result) do
:ok ->
IO.puts(" undo #{name}: ok")
{:error, reason} ->
IO.puts(" undo #{name}: failed (#{inspect(reason)}), retrying")
compensate(step)
end
end
end
trip = fn travel ->
[
{:flight, fn -> Travel.book(travel, :flight, "LIS-OSL") end, &Travel.cancel(travel, &1)},
{:hotel, fn -> Travel.book(travel, :hotel, "Oslo, 3 nights") end, &Travel.cancel(travel, &1)},
{:car, fn -> Travel.book(travel, :car, "compact") end, &Travel.cancel(travel, &1)}
]
end