This post is mirrored from it's original location on the Ash Discord.
I'm excited to announce the first release of Reactor - we've extracted the core ideas from Ash.Engine
into it's own package, added compensation and turned it into a dynamic, concurrent, dependency resolving saga orchestrator.
With reactor you break up your workflow into a bunch of steps and define the dependencies between then using arguments. Reactor will calculate the dependencies and run steps concurrency as their dependencies are fulfilled until there are no more steps left running.
Here's an example of a very simple step:
defmodule Greeter do
use Reactor.Step
def run(%{whom: nil}, _, _), do: {:ok, "Hello, World!"}
def run(%{whom: whom}, _, _), do: {:ok, "Hello, #{whom}!"}
end
You can construct a Reactor statically using a nice DSL
defmodule HelloWorldReactor do
use Reactor
input :whom
step :greet, Greeter do
argument :whom, input(:whom)
end
return :greet
end
iex> Reactor.run(HelloWorldReactor, %{whom: "Dear Reader"})
{:ok, "Hello, Dear Reader!"}
or you can build it programmatically:
iex> reactor = Builder.new()
...> {:ok, reactor} = Builder.add_input(reactor, :whom)
...> {:ok, reactor} = Builder.add_step(reactor, :greet, Greeter, whom: {:input, :whom})
...> {:ok, reactor} = Builder.return(reactor, :greet)
...> Reactor.run(reactor, %{whom: nil})
{:ok, "Hello, World!"}
Rollback
If any step fails and it defines the compensate/4
callback, Reactor will call the compensation function, giving the step the opportunity to recover, retry or clean up after itself. If the step is unable to recover, then any previously executed steps which define the undo/4
callback will be called. This allows for transaction-like semantics even when working with multiple disparate resources.
Summary
Shipping Reactor was a huge undertaking and I'm very proud of the result. Both Zach and I are very eager to start using it to replace the existing Ash.Engine
and unlocking new features of the Ash community.