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.