defmodule Runbox.StateStore.Savepoint do
@moduledoc group: :internal
@moduledoc """
Savepoint represents state of runtime run valid to scheduled timestamp.
"""
alias __MODULE__, as: Savepoint
alias Runbox.Runtime.Stage.UnitRegistry
alias Runbox.StateStore.Entity
alias Runbox.StateStore.ScheduleUtils
defstruct timestamp: 0,
stats: %{registered: 0, saved: 0},
entity_registry: %{}
@type t :: %Savepoint{
timestamp: ScheduleUtils.epoch_ms(),
stats: %{registered: integer, saved: integer},
entity_registry: %{Entity.id() => :not_set_yet | Entity.state()}
}
@doc """
Creates new savepoint with given timestamp and register entities.
"""
@spec new([Entity.id()], ScheduleUtils.epoch_ms()) :: t
def new(entity_ids, timestamp) do
%Savepoint{
timestamp: timestamp,
entity_registry: Map.new(entity_ids, fn entity_id -> {entity_id, :not_set_yet} end),
stats: %{registered: Enum.count(entity_ids), saved: 0}
}
end
@doc """
Returns savepoint timestamp.
"""
@spec timestamp(t) :: ScheduleUtils.epoch_ms()
def timestamp(%Savepoint{} = savepoint) do
savepoint.timestamp
end
@doc """
Returns all registered entity ids.
"""
@spec registered_entities(t) :: [Entity.id()]
def registered_entities(%Savepoint{} = savepoint) do
Map.keys(savepoint.entity_registry)
end
@doc """
Saves entity state in given savepoint.
"""
@spec save(t, Entity.id(), Entity.state()) :: t
def save(%Savepoint{entity_registry: entity_registry} = savepoint, entity_id, entity_state) do
case Map.fetch(entity_registry, entity_id) do
{:ok, :not_set_yet} ->
state = erase_nonpersisted_values(entity_state)
entity_registry = Map.put(entity_registry, entity_id, state)
stats = %{savepoint.stats | saved: savepoint.stats.saved + 1}
%Savepoint{savepoint | entity_registry: entity_registry, stats: stats}
{:ok, _} ->
# entity state has been already set = ignore
savepoint
:error ->
# entity is not registered = ignore
savepoint
end
end
@doc """
Returns entities with saved states.
"""
@spec entity_states(t) :: [{Entity.id(), Entity.state()}]
def entity_states(%Savepoint{entity_registry: entity_registry}) do
Map.to_list(entity_registry)
end
@doc """
Predicate returning true when all registered states has been saved.
"""
@spec all_saved?(t) :: boolean
def all_saved?(%Savepoint{stats: %{registered: r, saved: s}}) do
r == s
end
@doc """
Returns registered entities count of given savepoint.
"""
@spec registered_entities_count(t) :: integer
def registered_entities_count(%Savepoint{stats: %{registered: r}}) do
r
end
# public only for tests
@doc false
@spec fetch_entity_state(t, Entity.id()) :: {:ok, :not_set_yet | Entity.state()} | :error
def fetch_entity_state(%Savepoint{} = savepoint, entity_id) do
Map.fetch(savepoint.entity_registry, entity_id)
end
# public only for tests
@doc false
@spec saved_entities_count(t) :: integer
def saved_entities_count(%Savepoint{stats: %{saved: s}}) do
s
end
defp erase_nonpersisted_values(%UnitRegistry{} = registry) do
%UnitRegistry{registry | parse_msg_fns: nil, register_unit_fns: nil}
end
defp erase_nonpersisted_values(other), do: other
end