Packages
finitomata
0.40.0
0.41.0
0.40.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.30.3
0.30.2
0.30.1
0.30.0
0.29.10
0.29.9
0.29.8
0.29.7
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.4
0.26.3
0.26.2
0.26.1
0.26.0
0.25.0
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.7
0.23.6
0.23.5
0.23.4
0.23.3
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.1
0.15.0
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.1
0.1.0
The FSM implementation generated from PlantUML textual representation.
Current section
Files
Jump to
Current section
Files
lib/finitomata/engine.ex
defmodule Finitomata.Engine do
@moduledoc false
# Shared, process-agnostic runtime helpers used by every `use Finitomata` instance.
#
# Historically the whole FSM runtime was generated verbatim into each consuming module
# by the `use Finitomata` macro. That made the generated code huge, hard to test, and
# produced stacktraces pointing into macro-generated functions. This module is the seam
# for moving that logic into a single shared, directly unit-testable place.
#
# The functions here run *inside* the FSM process (they are ordinary function calls, not a
# separate server), therefore `self()` and the process dictionary still refer to the FSM
# itself. The transition orchestration (`transit/3`, `fork/3`, `maybe_store/7`,
# `maybe_pubsub/3`) and the stateless helpers live here; each FSM module keeps only thin
# delegations plus the per-module `telemetria`-instrumented `safe_on_*` callbacks that the
# Engine calls back into via the passed-in `module`. `init/1`, the `GenServer` callback
# shells, and the compile-time FSM setup remain generated, as they are coupled to
# compile-time configuration.
require Logger
alias Finitomata.{State, Transition}
@typedoc "An entry of the bounded state history; consecutive re-entries collapse into a tuple"
@type history_entry :: Transition.state() | {Transition.state(), pos_integer()}
@doc """
Normalizes an auto-driven event payload into a map carrying a `__retries__` counter,
incrementing it on every pass.
This is used for the transitions `Finitomata` drives itself (the initial entry transition,
`hard`/banged transitions, and `ensure_entry` retries). A non-map payload `p` becomes
`%{payload: p, __retries__: n}`. See `c:Finitomata.on_transition/4`.
iex> Finitomata.Engine.event_payload(:go)
{:go, %{__retries__: 1}}
iex> Finitomata.Engine.event_payload({:go, %{foo: :bar}})
{:go, %{foo: :bar, __retries__: 1}}
iex> Finitomata.Engine.event_payload({:go, 42})
{:go, %{payload: 42, __retries__: 1}}
iex> Finitomata.Engine.event_payload({:go, %{__retries__: 2}})
{:go, %{__retries__: 3}}
"""
@spec event_payload(Transition.event() | {Transition.event(), Finitomata.event_payload()}) ::
{Transition.event(), Finitomata.event_payload()}
def event_payload({event, %{} = payload}),
do: {event, Map.update(payload, :__retries__, 1, &(&1 + 1))}
def event_payload({event, payload}),
do: event_payload({event, %{payload: payload}})
def event_payload(event),
do: event_payload({event, %{}})
@doc """
Prepends `current` to the bounded `history`, collapsing consecutive re-entries of the
same state into a `{state, count}` tuple, and caps the result at
`Finitomata.State.history_size/0`.
iex> Finitomata.Engine.history(:a, [])
[:a]
iex> Finitomata.Engine.history(:a, [:a])
[{:a, 2}]
iex> Finitomata.Engine.history(:a, [{:a, 2}])
[{:a, 3}]
iex> Finitomata.Engine.history(:a, [:b])
[:a, :b]
"""
@spec history(Transition.state(), [history_entry()]) :: [history_entry()]
def history(current, history) do
history
|> case do
[^current | rest] -> [{current, 2} | rest]
[{^current, count} | rest] -> [{current, count + 1} | rest]
_ -> [current | history]
end
|> Enum.take(State.history_size())
end
@doc false
@spec safe_cancel_timer(false | {nil | reference(), pos_integer()}) ::
false | {nil, pos_integer()}
def safe_cancel_timer({ref, timer}) when is_integer(timer) and timer > 0 do
_ = if is_reference(ref), do: Process.cancel_timer(ref, async: true, info: false)
{nil, timer}
end
def safe_cancel_timer(_false), do: false
@doc false
@spec safe_init_timer(false | {nil | reference(), pos_integer()}) ::
false | {reference(), pos_integer()}
def safe_init_timer(timer) do
case safe_cancel_timer(timer) do
{nil, timer} when is_integer(timer) and timer > 0 ->
{Process.send_after(self(), :on_timer, timer), timer}
_ ->
false
end
end
@doc false
@spec hibernate_noreply(State.t()) ::
{:noreply, State.t()} | {:noreply, State.t(), :hibernate}
def hibernate_noreply(%State{hibernate: false} = state), do: {:noreply, state}
def hibernate_noreply(%State{} = state), do: {:noreply, state, :hibernate}
@doc false
@spec hibernate_reply(term(), State.t()) ::
{:reply, term(), State.t()} | {:reply, term(), State.t(), :hibernate}
def hibernate_reply(reply, %State{hibernate: false} = state), do: {:reply, reply, state}
def hibernate_reply(reply, %State{} = state), do: {:reply, reply, state, :hibernate}
@doc false
@spec transit(module(), {Transition.event(), Finitomata.event_payload()}, State.t()) ::
{:noreply, State.t()}
| {:noreply, State.t(), :hibernate}
| {:stop, :normal, State.t()}
def transit(module, {event, payload}, %State{} = state) do
fsm = module.__config__(:fsm)
with {:responds, true} <- {:responds, Transition.responds?(fsm, state.current, event)},
{:on_exit, :ok} <- {:on_exit, module.safe_on_exit(state.current, state)},
{:ok, new_current, new_payload} <-
module.safe_on_transition(state.name, state.current, event, payload, state.payload),
new_timer <- safe_cancel_timer(state.timer),
{:allowed, true} <- {:allowed, Transition.allowed?(fsm, state.current, new_current)},
new_history = history(state.current, state.history),
state = %{
state
| payload: new_payload,
current: new_current,
history: new_history,
timer: safe_init_timer(new_timer)
},
{:on_enter, :ok} <- {:on_enter, module.safe_on_enter(new_current, state)} do
if module.__config__(:cache_state),
do: Finitomata.StateCache.put(state.finitomata_id, state.name, state.payload)
cond do
new_current == :* ->
{:stop, :normal, state}
new_current in module.__config__(:hard_states) ->
{:noreply, state,
{:continue, {:transition, event_payload(module.__config__(:hard)[new_current].event)}}}
new_current in module.__config__(:fork_states) ->
{:noreply, state, {:continue, {:fork, new_current}}}
true ->
hibernate_noreply(state)
end
else
{err, false} ->
Logger.warning(
"[⚐ ↹] transition from #{state.current} with #{event} does not exists or not allowed (:#{err})"
)
module.safe_on_failure(event, payload, state)
hibernate_noreply(state)
{err, :ok} ->
Logger.warning("[⚐ ↹] callback failed to return `:ok` (:#{err})")
module.safe_on_failure(event, payload, state)
hibernate_noreply(state)
err ->
state = %{
state
| last_error:
Finitomata.Error.wrap(err,
state: state.current,
event: event,
event_payload: payload
)
}
cond do
event in module.__config__(:soft_events) ->
Logger.debug("[⚐ ↹] transition softly failed " <> inspect(err))
hibernate_noreply(state)
fsm
|> Transition.allowed(state.current, event)
|> Enum.all?(&(&1 in module.__config__(:ensure_entry))) ->
{:noreply, state, {:continue, {:transition, event_payload({event, payload})}}}
true ->
Logger.warning("[⚐ ↹] transition failed " <> inspect(err))
module.safe_on_failure(event, payload, state)
hibernate_noreply(state)
end
end
end
@doc false
@spec fork(module(), Transition.state(), State.t()) ::
{:noreply, State.t()} | {:noreply, State.t(), :hibernate}
def fork(module, fork_state, %State{} = state) do
fork_data =
case state.payload do
%{fork_data: %{} = fork_data} -> fork_data
_ -> %{}
end
{object, fork_data} = Map.pop(fork_data, :object)
{id, fork_data} = Map.pop(fork_data, :id)
# [TODO] [AM] Match the outcome and log it
_ =
module.__config__(:forks)
|> Keyword.fetch!(fork_state)
|> List.wrap()
|> module.safe_on_fork(fork_state, state)
|> case do
{:ok, fork_impl, event} ->
fsm_name = Finitomata.fsm_name(state)
Finitomata.start_fsm(
state.finitomata_id,
fork_impl,
{:fork, fork_state, fsm_name},
%{
owner: %{
event: event,
id: state.finitomata_id,
name: fsm_name,
pid: Finitomata.pid(state)
},
history: %{current: 0, steps: []},
steps: %{passed: 0, left: Transition.steps_handled(fork_impl.__config__(:fsm))},
object: object,
id: id,
data: fork_data
}
)
{:error, error} ->
Logger.warning("[⚐ ↹] fork from #{fork_state} failed (#{inspect(error)})")
end
hibernate_noreply(state)
end
@doc false
@spec maybe_store(
module(),
Finitomata.transition_resolution(),
Finitomata.fsm_name(),
Transition.state(),
Transition.event(),
Finitomata.event_payload(),
State.payload()
) :: Finitomata.transition_resolution()
def maybe_store(module, result, name, current, event, event_payload, state_payload) do
case module.__config__(:persistency) do
nil ->
result
persistency ->
do_maybe_store(persistency, result, name, current, event, event_payload, state_payload)
end
end
defp do_maybe_store(
persistency,
{:error, reason},
name,
current,
event,
event_payload,
state_payload
) do
with true <- function_exported?(persistency, :store_error, 4),
info = %{
from: current,
to: nil,
event: event,
event_payload: event_payload,
object: state_payload
},
{:error, persistency_error_reason} <-
persistency.store_error(name, state_payload, reason, info) do
{:error, transition: reason, persistency: persistency_error_reason}
else
_ -> {:error, transition: reason}
end
end
defp do_maybe_store(
persistency,
{:ok, new_state, new_state_payload} = result,
name,
current,
event,
event_payload,
state_payload
) do
info = %{
from: current,
to: new_state,
event: event,
event_payload: event_payload,
object: state_payload
}
name
|> persistency.store(new_state_payload, info)
|> case do
:ok -> result
{:ok, updated_state_payload} -> {:ok, new_state, updated_state_payload}
{:error, reason} -> {:error, persistency: reason}
end
end
defp do_maybe_store(
_persistency,
result,
_name,
_current,
_event,
_event_payload,
_state_payload
),
do: {:error, transition: result}
@doc false
@spec maybe_pubsub(module(), Finitomata.transition_resolution(), Finitomata.fsm_name()) :: :ok
def maybe_pubsub(module, result, name),
do: do_maybe_pubsub(module.__config__(:listener), result, name)
defp do_maybe_pubsub(nil, _result, _name), do: :ok
defp do_maybe_pubsub(listener, {:ok, state, payload}, name) do
cond do
is_atom(listener) and function_exported?(listener, :after_transition, 3) ->
with some when some != :ok <- listener.after_transition(name, state, payload) do
Logger.warning(
"[♻️] Listener ‹" <>
inspect(Function.capture(listener, :after_transition, 3)) <>
"› returned unexpected ‹" <>
inspect(some) <>
"› when called with ‹" <> inspect([name, state, payload]) <> "›"
)
end
:ok
is_pid(listener) or is_port(listener) or is_atom(listener) or is_tuple(listener) ->
send(listener, {:finitomata, {:transition, state, payload}})
:ok
true ->
:ok
end
end
defp do_maybe_pubsub(_listener, _result, _name), do: :ok
@doc false
@spec handle_call(module(), term(), State.t()) ::
{:reply, term(), State.t()} | {:reply, term(), State.t(), :hibernate}
def handle_call(_module, :state, %State{} = state), do: hibernate_reply(state, state)
def handle_call(_module, {:state, fun}, %State{} = state) when is_function(fun, 1),
do: hibernate_reply(fun.(state), state)
def handle_call(_module, :current_state, %State{} = state),
do: hibernate_reply(state.current, state)
def handle_call(_module, :name, %State{} = state),
do: hibernate_reply(State.human_readable_name(state, false), state)
def handle_call(module, {:allowed?, to}, %State{} = state),
do: hibernate_reply(Transition.allowed?(module.__config__(:fsm), state.current, to), state)
def handle_call(module, {:responds?, event}, %State{} = state),
do:
hibernate_reply(Transition.responds?(module.__config__(:fsm), state.current, event), state)
def handle_call(_module, whatever, %State{} = state) do
Logger.error(
"Unexpected `GenServer.call/2` with a message ‹#{inspect(whatever)}›. " <>
"`Finitomata` does not accept direct calls. Please use `on_transition/4` callback instead."
)
hibernate_reply(:not_allowed, state)
end
@doc false
@spec handle_cast(module(), term(), State.t()) ::
{:noreply, State.t()}
| {:noreply, State.t(), :hibernate}
| {:noreply, State.t(), {:continue, term()}}
def handle_cast(_module, {:reset_timer, tick?, _new_value}, %State{} = state) do
timer =
if tick? do
_ = safe_cancel_timer(state.timer)
:ok = Process.send(self(), :on_timer, [])
state.timer
else
safe_init_timer(state.timer)
end
hibernate_noreply(%{state | timer: timer})
end
def handle_cast(_module, {event, payload}, %State{} = state),
do: {:noreply, state, {:continue, {:transition, {event, payload}}}}
def handle_cast(_module, whatever, %State{} = state) do
Logger.error(
"Unexpected `GenServer.cast/2` with a message ‹#{inspect(whatever)}›. " <>
"`Finitomata` does not accept direct casts. Please use `on_transition/4` callback instead."
)
hibernate_noreply(state)
end
@doc false
@spec handle_continue(module(), term(), State.t()) ::
{:noreply, State.t()}
| {:noreply, State.t(), :hibernate}
| {:stop, :normal, State.t()}
def handle_continue(module, {:transition, {event, payload}}, %State{} = state),
do: transit(module, {event, payload}, state)
def handle_continue(module, {:fork, fork_state}, %State{} = state),
do: fork(module, fork_state, state)
@doc false
@spec terminate(module(), term(), State.t()) :: term()
def terminate(module, _reason, %State{} = state) do
if module.__config__(:cache_state),
do: Finitomata.StateCache.delete(state.finitomata_id, state.name)
module.safe_on_terminate(state)
end
@doc false
@spec handle_info(module(), term(), State.t()) ::
{:noreply, State.t()} | {:noreply, State.t(), :hibernate}
def handle_info(_module, whatever, %State{} = state)
when not is_tuple(state.timer) or not is_integer(elem(state.timer, 1)) or
whatever != :on_timer do
Logger.error(
"Unexpected message ‹#{inspect(whatever)}› received by #{inspect(State.human_readable_name(state))}. " <>
"`Finitomata` does not accept direct messages. Please use `on_transition/4` callback instead."
)
hibernate_noreply(state)
end
def handle_info(module, :on_timer, %State{} = state) do
if module.__config__(:timer),
do: do_on_timer(module, state),
else: on_timer_undeclared(state)
end
defp on_timer_undeclared(%State{} = state) do
Logger.warning("[⚑ ↹] on_timer message received, but no `on_timer/2` callback is declared")
hibernate_noreply(state)
end
defp do_on_timer(module, %State{} = state) do
state.current
|> module.safe_on_timer(state)
|> case do
:ok ->
hibernate_noreply(state)
{:ok, state_payload} ->
if module.__config__(:cache_state),
do: Finitomata.StateCache.put(state.finitomata_id, state.name, state_payload)
hibernate_noreply(%{state | payload: state_payload})
{:transition, {event, event_payload}, state_payload} ->
transit(module, {event, event_payload}, %{state | payload: state_payload})
{:transition, event, state_payload} ->
transit(module, {event, nil}, %{state | payload: state_payload})
{:reschedule, value} ->
timer = with {ref, _old_value} <- state.timer, do: {ref, value}
hibernate_noreply(%{state | timer: timer})
weird ->
Logger.warning("[⚑ ↹] on_timer returned a garbage " <> inspect(weird))
hibernate_noreply(state)
end
|> then(fn
{:noreply, %State{timer: timer} = state} ->
hibernate_noreply(%{state | timer: safe_init_timer(timer)})
other ->
other
end)
end
@doc false
@spec init(module(), map()) ::
{:ok, State.t()} | {:ok, State.t(), {:continue, term()}} | {:stop, keyword()}
def init(
module,
%{
finitomata_id: id,
name: name,
parent: parent,
payload: payload,
with_persistency: persistency
} = state
)
when not is_nil(name) and not is_nil(persistency) do
{lifecycle, {state, payload}} =
case payload do
mod when is_atom(mod) ->
persistency.load({payload, id: name})
%struct{} = payload ->
persistency.load({struct, payload |> Map.from_struct() |> Map.put_new(:id, name)})
%{type: type, id: id} ->
persistency.load({type, %{id => name}})
other ->
Logger.warning(
"Loading from persisted for ‹#{inspect(state)}› failed; wrong payload: " <>
inspect(other)
)
{:failed, {nil, other}}
end
init(module, %{
name: name,
finitomata_id: id,
parent: parent,
state: state,
payload: payload,
lifecycle: lifecycle,
persistency: persistency
})
end
def init(module, %{payload: payload} = init_arg) do
lifecycle = Map.get(init_arg, :lifecycle, :unknown)
{state, init_arg} = Map.pop(init_arg, :state)
init_state =
if is_nil(state) or lifecycle in [:failed, :created] do
init_arg
|> module.safe_on_start(payload)
|> case do
{:stop, reason} -> {:stop, :on_start, reason}
{:ok, payload} -> {nil, payload}
{:continue, payload} -> {nil, payload}
_ -> {nil, payload}
end
else
{state, payload}
end
do_init(module, init_state, init_arg)
end
@spec do_init(module(), tuple(), map()) ::
{:ok, State.t()} | {:ok, State.t(), {:continue, term()}} | {:stop, keyword()}
defp do_init(_module, {:stop, :on_start, reason}, init_arg),
do: {:stop, reason: reason, init_arg: init_arg}
defp do_init(
module,
{state, payload},
%{finitomata_id: id, name: name, parent: parent} = init_arg
) do
lifecycle = Map.get(init_arg, :lifecycle, :unknown)
timer = safe_init_timer({nil, module.__config__(:timer)})
state =
%State{
name: name,
finitomata_id: id,
parent: parent,
lifecycle: lifecycle,
persistency: Map.get(init_arg, :persistency, nil),
timer: timer,
cache_state: module.__config__(:cache_state),
hibernate: module.__config__(:hibernate),
payload: payload
}
|> put_current_state_if_loaded(lifecycle, state)
if module.__config__(:cache_state),
do: Finitomata.StateCache.put(state.finitomata_id, state.name, state.payload)
if lifecycle == :loaded,
do: {:ok, state},
else:
{:ok, state, {:continue, {:transition, event_payload({module.__config__(:entry), nil})}}}
end
defp put_current_state_if_loaded(state, :loaded, fsm_state)
when not is_nil(fsm_state),
do: Map.put(state, :current, fsm_state)
defp put_current_state_if_loaded(state, _, _fsm_state), do: state
# Per-callback wrappers. Each generated FSM module keeps a thin, `@telemetria`-annotated
# `safe_on_*` shim (so telemetry events stay grouped by the consumer module) that
# delegates here. Centralizing the bodies keeps the `use Finitomata` quote small while
# preserving the exact callback-resolution, error-wrapping, and return semantics.
@doc false
@spec safe_on_start(module(), :loaded | map(), State.payload()) ::
{:stop, term()} | {:continue, State.payload()} | {:ok, State.payload()} | :ignore
def safe_on_start(_module, :loaded, payload), do: {:ok, payload}
def safe_on_start(module, _state, payload) do
if function_exported?(module, :on_start, 1),
do: module.on_start(payload),
else: :ignore
rescue
err ->
_ = report_error(err, __STACKTRACE__, "on_start/1")
{:stop, err}
end
@doc false
@spec safe_on_transition(
module(),
Finitomata.fsm_name(),
Transition.state(),
Transition.event(),
Finitomata.event_payload(),
State.payload()
) :: Finitomata.transition_resolution() | {:error, :on_transition_raised}
def safe_on_transition(module, name, current, event, event_payload, state_payload) do
module.on_transition(current, event, event_payload, state_payload)
|> then(&maybe_store(module, &1, name, current, event, event_payload, state_payload))
|> tap(&maybe_pubsub(module, &1, name))
rescue
err -> report_error(err, __STACKTRACE__, "on_transition/4")
end
@doc false
@spec safe_on_fork(module(), [module()], Transition.state(), State.t()) ::
{:ok, module(), Transition.event()} | {:error, any()}
def safe_on_fork(module, forks, fork_state, state) do
cond do
function_exported?(module, :on_fork, 2) ->
case module.on_fork(fork_state, state.payload) do
{:ok, fork_impl} ->
case Enum.find(forks, &match?({_event, ^fork_impl}, &1)) do
nil -> {:error, :unknown_fork_resolution}
{event, ^fork_impl} -> {:ok, fork_impl, event}
end
:ok ->
case forks do
[{event, fork_impl}] -> {:ok, fork_impl, event}
[] -> {:error, :missing_fork_resolution}
_ -> {:error, :multiple_fork_resolutions}
end
_other ->
{:error, :bad_fork_resolution}
end
match?([_fork], forks) ->
[{event, fork_impl}] = forks
{:ok, fork_impl, event}
true ->
{:error, :missing_fork_resolution}
end
rescue
err ->
_ = report_error(err, __STACKTRACE__, "on_fork/2")
{:error, :on_fork_raised}
end
@doc false
@spec safe_on_failure(module(), Transition.event(), Finitomata.event_payload(), State.t()) ::
:ok
def safe_on_failure(module, event, event_payload, state_payload) do
if function_exported?(module, :on_failure, 3) do
with other when other != :ok <-
module.on_failure(event, event_payload, state_payload) do
Logger.info("[♻️] Unexpected return from a callback [#{inspect(other)}], must be :ok")
:ok
end
else
:ok
end
rescue
err -> report_error(err, __STACKTRACE__, "on_failure/3")
end
@doc false
@spec safe_on_enter(module(), Transition.state(), State.t()) :: :ok
def safe_on_enter(module, state, state_payload) do
if function_exported?(module, :on_enter, 2) do
with other when other != :ok <- module.on_enter(state, state_payload) do
Logger.info("[♻️] Unexpected return from a callback [#{inspect(other)}], must be :ok")
:ok
end
else
:ok
end
rescue
err -> report_error(err, __STACKTRACE__, "on_enter/2")
end
@doc false
@spec safe_on_exit(module(), Transition.state(), State.t()) :: :ok
def safe_on_exit(module, state, state_payload) do
if function_exported?(module, :on_exit, 2) do
with other when other != :ok <- module.on_exit(state, state_payload) do
Logger.info("[♻️] Unexpected return from a callback [#{inspect(other)}], must be :ok")
:ok
end
else
:ok
end
rescue
err -> report_error(err, __STACKTRACE__, "on_exit/2")
end
@doc false
@spec safe_on_terminate(module(), State.t()) :: :ok
def safe_on_terminate(module, state) do
if function_exported?(module, :on_terminate, 1) do
with other when other != :ok <- module.on_terminate(state) do
Logger.warning(
"[♻️] Unexpected return from `on_terminate/1` [#{inspect(other)}], must be :ok"
)
end
else
:ok
end
rescue
err -> report_error(err, __STACKTRACE__, "on_terminate/1")
end
@doc false
@spec safe_on_timer(module(), Transition.state(), State.t()) ::
:ok
| {:ok, State.t()}
| {:transition, {Transition.state(), Finitomata.event_payload()}, State.payload()}
| {:transition, Transition.state(), State.payload()}
| {:reschedule, pos_integer()}
def safe_on_timer(module, state, state_payload) do
if function_exported?(module, :on_timer, 2),
do: module.on_timer(state, state_payload),
else: :ok
rescue
err -> report_error(err, __STACKTRACE__, "on_timer/2")
end
@spec report_error(term(), Exception.stacktrace(), String.t()) :: {:error, term()}
defp report_error(err, stacktrace, from) do
case err do
%{__exception__: true} ->
{ex, st} = Exception.blame(:error, err, stacktrace)
Logger.warning(Exception.format(:error, ex, st))
{:error, Exception.message(err)}
_ ->
Logger.warning("[⚑ ↹] #{from} raised: " <> inspect(err) <> "\n" <> inspect(stacktrace))
{:error, :on_transition_raised}
end
end
end