Packages
altworx_runbox
7.0.0
25.0.0
24.0.0
23.1.0
23.0.0
22.2.0
22.1.0
22.0.0
21.2.0
21.1.2
21.1.1
21.1.0
21.0.0
20.0.0
19.0.0
18.0.0
17.2.0
17.1.0
17.0.1
17.0.0
16.2.0
16.1.0
16.0.0
15.0.0
14.1.0
14.0.1
14.0.0
13.0.3
13.0.2
13.0.1
13.0.0
12.1.0
12.0.0
11.0.1
11.0.0
10.0.0
9.0.0
8.0.0
7.0.1
7.0.0
6.0.0
5.0.0
4.0.0
3.0.0
2.1.0
2.0.0
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.1.3
0.1.2
0.1.1
0.1.0
Runbox is a library for running Altworx scenarios.
Current section
Files
Jump to
Current section
Files
lib/runbox/state_store/entity.ex
defmodule Runbox.StateStore.Entity do
@moduledoc """
Module defines struct which acts as state store envelope for any stateful runtime component.
"""
alias __MODULE__, as: Entity
alias Runbox.StateStore.ScheduleUtils
defstruct run_id: nil,
id: nil,
state: nil,
timestamp: nil,
schedule: nil,
next_savepoint: nil,
last_savepoint: nil
@type id :: String.t() | atom
@type state :: term
@type t :: %Entity{
run_id: String.t(),
id: id,
state: state,
timestamp: ScheduleUtils.epoch_ms(),
schedule: ScheduleUtils.schedule(),
next_savepoint: ScheduleUtils.epoch_ms(),
last_savepoint: ScheduleUtils.epoch_ms()
}
@doc "Creates new state store entity"
@spec new(String.t(), id, ScheduleUtils.schedule(), ScheduleUtils.epoch_ms(), state) :: t
def new(run_id, id, schedule, timestamp, state) do
next_savepoint =
if schedule == :none, do: :none, else: ScheduleUtils.next_savepoint(schedule, timestamp)
%Entity{
run_id: run_id,
id: id,
schedule: schedule,
state: state,
timestamp: timestamp,
last_savepoint: timestamp,
next_savepoint: next_savepoint
}
end
@doc "Returns entity id"
@spec id(t) :: id
def id(%Entity{id: id}) do
id
end
@doc "Returns entity state"
@spec state(t) :: state
def state(%Entity{state: state}) do
state
end
@doc "Returns entity timestamp"
@spec timestamp(t) :: ScheduleUtils.epoch_ms()
def timestamp(%Entity{timestamp: timestamp}) do
timestamp
end
@doc "Returns entity schedule"
@spec schedule(t) :: ScheduleUtils.schedule()
def schedule(%Entity{schedule: schedule}) do
schedule
end
@doc """
Set entity `state` at the specified point in time.
"""
@spec update_state(t, ScheduleUtils.epoch_ms(), state) :: t
def update_state(%Entity{timestamp: current_ts} = entity, timestamp, state) do
# Prevent moving back to history due to erroneous timestamp
new_ts = max(current_ts, timestamp)
%Entity{entity | timestamp: new_ts, state: state}
end
@doc """
Confirm that all messages with lower timestamps were processed.
When the timestamp is higher than next savepoint the state is persisted into `StateStore` with
`StateStore.save/4` and next savepoint target is determined and stored in the entity.
The state is persisted only once per every savepoint.
"""
@spec ack_processed_time(t, ScheduleUtils.epoch_ms(), {module, atom, list}) ::
{:ok, t} | {:error, term}
def ack_processed_time(%Entity{schedule: schedule} = entity, timestamp, save_mfa) do
if should_persist?(entity, timestamp) do
# subtracting 1 from the timestamp, because it says everything up until timestamp is
# processed, so the last fully processed time is `timestamp - 1` and ScheduleUtils works with
# processed time
last_savepoint = ScheduleUtils.previous_savepoint(schedule, timestamp - 1)
next_savepoint = ScheduleUtils.next_savepoint(schedule, timestamp - 1)
{mod, fun, args} = save_mfa
:ok = apply(mod, fun, args ++ [entity, last_savepoint])
{:ok, %Entity{entity | last_savepoint: last_savepoint, next_savepoint: next_savepoint}}
else
{:ok, entity}
end
end
# public only for tests
@doc false
@spec latest_persisted_savepoint(t) :: ScheduleUtils.epoch_ms()
def latest_persisted_savepoint(%Entity{} = entity) do
entity.last_savepoint
end
@spec should_persist?(t, ScheduleUtils.epoch_ms()) :: boolean
defp should_persist?(%Entity{} = entity, now) do
# we are beyond the next_savepoint, thus all messages that belong to the savepoint were
# processed
now > entity.next_savepoint
end
end