Packages
reactor
1.0.1
1.0.2
1.0.1
1.0.0
0.17.0
0.16.0
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
An asynchronous, graph-based execution engine
Current section
Files
Jump to
Current section
Files
lib/reactor/executor/sync.ex
# SPDX-FileCopyrightText: 2023 James Harton, Zach Daniel, Alembic Pty and contributors
# SPDX-FileCopyrightText: 2023 reactor contributors <https://github.com/ash-project/reactor/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule Reactor.Executor.Sync do
@moduledoc """
Handle the synchronous execution of a single step, along with any mutations to
the reactor or execution state.
"""
alias Reactor.Error.Invalid.RetriesExceededError, as: RetriesExceededError
alias Reactor.{Executor, Step}
@doc """
Try and run a step synchronously.
"""
@spec run(Reactor.t(), Executor.State.t(), Step.t() | nil) ::
{:continue | :recurse | :halt | :undo, Reactor.t(), Executor.State.t()}
def run(reactor, state, nil), do: {:continue, reactor, state}
def run(reactor, state, step) do
case Executor.StepRunner.run(reactor, state, step, state.concurrency_key) do
{:skip, result} ->
state = %{state | skipped: MapSet.put(state.skipped, step.ref)}
handle_completed_step(reactor, state, step, result)
result ->
handle_completed_step(reactor, state, step, result)
end
end
defp handle_completed_step(reactor, state, step, {:backoff, delay, result}) do
backoff = Executor.Backoff.delay(delay)
plan =
reactor.plan
|> Graph.add_vertex(backoff)
|> Graph.add_edge(backoff, step, label: :backoff)
reactor = %{reactor | plan: plan}
handle_completed_step(reactor, state, step, result)
end
defp handle_completed_step(reactor, state, step, :retry) do
handle_completed_step(reactor, state, step, {:retry, nil})
end
defp handle_completed_step(reactor, state, step, {:retry, error}) do
state = increment_retries(state, step)
if Map.get(state.retries, step.ref) > step.max_retries do
reactor = drop_from_plan(reactor, step)
error =
error ||
RetriesExceededError.exception(
step: step,
retry_count: Map.get(state.retries, step.ref)
)
{:undo, reactor, %{state | errors: [error | state.errors]}}
else
{:recurse, reactor, state}
end
end
defp handle_completed_step(reactor, state, step, {:ok, value, new_steps}) do
reactor =
reactor
|> maybe_store_undo(step, value, state)
|> maybe_store_intermediate_result(step, value)
reactor =
case Enum.split_with(new_steps, &(&1.name == step.name)) do
{[], new_steps} ->
reactor
|> drop_from_plan(step)
|> append_steps(new_steps)
{recursive_steps, new_steps} ->
recursive_steps = Enum.map(recursive_steps, &%{&1 | ref: step.ref})
reactor
|> store_intermediate_result(step, value)
|> append_steps(recursive_steps)
|> append_steps(new_steps)
end
{:recurse, reactor, state}
end
defp handle_completed_step(reactor, state, step, {:error, reason}) do
state = %{state | errors: [reason | state.errors]}
reactor = drop_from_plan(reactor, step)
{:undo, reactor, state}
end
defp handle_completed_step(reactor, state, step, {:halt, value}) do
reactor =
reactor
|> drop_from_plan(step)
|> store_intermediate_result(step, value)
{:halt, reactor, state}
end
defp increment_retries(state, step) do
%{state | retries: Map.update(state.retries, step.ref, 1, &(&1 + 1))}
end
defp drop_from_plan(reactor, step) do
%{reactor | plan: Graph.delete_vertex(reactor.plan, step)}
end
defp maybe_store_undo(reactor, step, value, state) do
cond do
MapSet.member?(state.skipped, step.ref) -> reactor
Step.can?(step, :undo) -> %{reactor | undo: [{step, value} | reactor.undo]}
true -> reactor
end
end
defp maybe_store_intermediate_result(reactor, step, value) when reactor.return == step.name do
store_intermediate_result(reactor, step, value)
end
defp maybe_store_intermediate_result(reactor, step, value) do
if Graph.out_degree(reactor.plan, step) > 0 do
store_intermediate_result(reactor, step, value)
else
reactor
end
end
defp store_intermediate_result(reactor, step, value),
do: %{reactor | intermediate_results: Map.put(reactor.intermediate_results, step.name, value)}
defp append_steps(reactor, steps), do: %{reactor | steps: Enum.concat(steps, reactor.steps)}
end