2023-04-19 10:23:04 +12:00
|
|
|
defmodule Reactor do
|
2023-05-12 19:47:36 +12:00
|
|
|
alias Reactor.{Dsl, Executor, Info, Planner, Step}
|
|
|
|
|
|
|
|
@moduledoc """
|
2023-05-10 16:27:18 +12:00
|
|
|
Reactor is a dynamic, concurrent, dependency resolving saga orchestrator.
|
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
|
|
You can construct a reactor using the `Reactor` Spark DSL:
|
|
|
|
|
|
|
|
```elixir
|
|
|
|
defmodule HelloWorldReactor do
|
|
|
|
@moduledoc false
|
|
|
|
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!"}
|
|
|
|
|
2023-05-12 19:47:36 +12:00
|
|
|
<!--- ash-hq-hide-start --> <!--- -->
|
|
|
|
|
|
|
|
## DSL Documentation
|
|
|
|
|
|
|
|
### Index
|
|
|
|
|
|
|
|
#{Spark.Dsl.Extension.doc_index(Dsl.sections())}
|
|
|
|
|
|
|
|
### Docs
|
|
|
|
|
|
|
|
#{Spark.Dsl.Extension.doc(Dsl.sections())}
|
|
|
|
|
|
|
|
<!--- ash-hq-hide-stop --> <!--- -->
|
2023-04-19 10:23:04 +12:00
|
|
|
"""
|
|
|
|
|
2023-05-10 16:27:18 +12:00
|
|
|
defstruct context: %{},
|
2023-06-12 16:24:09 +12:00
|
|
|
id: nil,
|
2023-05-10 16:27:18 +12:00
|
|
|
inputs: [],
|
|
|
|
intermediate_results: %{},
|
|
|
|
plan: nil,
|
|
|
|
return: nil,
|
|
|
|
state: :pending,
|
2023-06-12 16:24:09 +12:00
|
|
|
steps: [],
|
|
|
|
undo: []
|
2023-05-10 16:27:18 +12:00
|
|
|
|
|
|
|
use Spark.Dsl, default_extensions: [extensions: Dsl]
|
|
|
|
|
|
|
|
@type context :: Enumerable.t({any, any})
|
|
|
|
@type options ::
|
|
|
|
Enumerable.t(
|
|
|
|
{:max_concurrency, pos_integer()}
|
|
|
|
| {:timeout, pos_integer() | :infinity}
|
|
|
|
| {:max_iterations, pos_integer() | :infinity}
|
|
|
|
| {:halt_timeout, pos_integer() | :infinity}
|
|
|
|
)
|
|
|
|
|
|
|
|
@type state :: :pending | :executing | :halted | :failed | :successful
|
|
|
|
@type inputs :: %{optional(atom) => any}
|
2023-04-19 10:23:04 +12:00
|
|
|
|
2023-05-10 16:27:18 +12:00
|
|
|
@type t :: %Reactor{
|
|
|
|
context: context,
|
2023-06-12 16:24:09 +12:00
|
|
|
id: any,
|
2023-05-10 16:27:18 +12:00
|
|
|
inputs: [atom],
|
|
|
|
intermediate_results: %{any => any},
|
|
|
|
plan: nil | Graph.t(),
|
|
|
|
undo: [{Step.t(), any}],
|
|
|
|
return: any,
|
|
|
|
state: state,
|
|
|
|
steps: [Step.t()]
|
|
|
|
}
|
2023-04-19 10:23:04 +12:00
|
|
|
|
2023-05-10 16:27:18 +12:00
|
|
|
@doc false
|
|
|
|
@spec is_reactor(any) :: Macro.t()
|
|
|
|
defguard is_reactor(reactor) when is_struct(reactor, __MODULE__)
|
2023-04-19 10:23:04 +12:00
|
|
|
|
2023-05-10 16:27:18 +12:00
|
|
|
@doc """
|
|
|
|
Run a reactor.
|
2023-04-19 10:23:04 +12:00
|
|
|
"""
|
2023-05-10 16:27:18 +12:00
|
|
|
@spec run(t | module, inputs, context, options) :: {:ok, any} | {:error, any} | {:halted, t}
|
|
|
|
def run(reactor, inputs \\ %{}, context \\ %{}, options \\ [])
|
|
|
|
|
|
|
|
def run(reactor, inputs, context, options) when is_atom(reactor) do
|
|
|
|
with Reactor <- reactor.spark_is(),
|
|
|
|
{:ok, reactor} <- Info.to_struct(reactor) do
|
|
|
|
run(reactor, inputs, context, options)
|
|
|
|
end
|
|
|
|
rescue
|
|
|
|
UndefinedFunctionError -> {:error, "Module `#{inspect(reactor)}` is not a Reactor module"}
|
2023-04-19 10:23:04 +12:00
|
|
|
end
|
2023-05-10 16:27:18 +12:00
|
|
|
|
|
|
|
def run(reactor, inputs, context, options)
|
|
|
|
when is_reactor(reactor) and reactor.state in ~w[pending halted]a do
|
|
|
|
with {:ok, reactor} <- maybe_plan(reactor) do
|
|
|
|
Executor.run(reactor, inputs, context, options)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp maybe_plan(reactor) when reactor.steps == [], do: {:ok, reactor}
|
|
|
|
defp maybe_plan(reactor), do: Planner.plan(reactor)
|
2023-04-19 10:23:04 +12:00
|
|
|
end
|