farmbot_os/farmbot_core/lib/farmbot_core/asset_workers/regimen_instance_worker.ex

93 lines
3.4 KiB
Elixir

defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
@moduledoc """
An instance of a running Regimen. Asset.Regimen is the blueprint by which a
Regimen "instance" is created.
"""
use GenServer
require Logger
require FarmbotCore.Logger
alias FarmbotCeleryScript.AST
alias FarmbotCore.Asset
alias FarmbotCore.Asset.{RegimenInstance, FarmEvent, Sequence, Regimen}
@impl FarmbotCore.AssetWorker
def preload(%RegimenInstance{}), do: [:farm_event, :regimen, :executions]
@impl FarmbotCore.AssetWorker
def tracks_changes?(%RegimenInstance{}), do: false
@impl FarmbotCore.AssetWorker
def start_link(regimen_instance, args) do
GenServer.start_link(__MODULE__, [regimen_instance, args])
end
@impl GenServer
def init([regimen_instance, _args]) do
Logger.warn "RegimenInstance #{inspect(regimen_instance)} initializing"
with %Regimen{} <- regimen_instance.regimen,
%FarmEvent{} <- regimen_instance.farm_event do
send self(), :schedule
{:ok, %{regimen_instance: regimen_instance}}
else
_ -> {:stop, "Regimen instance not preloaded."}
end
end
@impl GenServer
def handle_info(:schedule, state) do
regimen_instance = state.regimen_instance
# load the sequence and calculate the scheduled_at time
Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) ->
scheduled_at = DateTime.add(regimen_instance.epoch, offset, :millisecond)
sequence = Asset.get_sequence(sequence_id) || raise("sequence #{sequence_id} is not synced")
%{scheduled_at: scheduled_at, sequence: sequence}
end)
# get rid of any item that has already been scheduled/executed
|> Enum.reject(fn(%{scheduled_at: scheduled_at}) ->
Asset.get_regimen_instance_execution(regimen_instance, scheduled_at)
end)
|> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) ->
schedule_sequence(regimen_instance, sequence, at)
end)
{:noreply, state}
end
def handle_info({FarmbotCeleryScript, {:scheduled_execution, scheduled_at, executed_at, result}}, state) do
status = case result do
:ok -> "ok"
{:error, reason} ->
FarmbotCore.Logger.error(2, "Regimen scheduled at #{scheduled_at} failed to execute: #{reason}")
reason
end
_ = Asset.add_execution_to_regimen_instance!(state.regimen_instance, %{
scheduled_at: scheduled_at,
executed_at: executed_at,
status: status
})
{:noreply, state}
end
# TODO(RickCarlino) This function essentially copy/pastes a regimen body into
# the `locals` of a sequence, which works but is not-so-clean. Refactor later
# when we have a better idea of the problem.
@doc false
def schedule_sequence(%RegimenInstance{} = regimen_instance, %Sequence{} = sequence, at) do
# FarmEvent is the furthest outside of the scope
farm_event_params = AST.decode(regimen_instance.farm_event.body)
# Regimen is the second scope
regimen_params = AST.decode(regimen_instance.regimen.body)
# there may be many sequence scopes from here downward
celery_ast = AST.decode(sequence)
celery_args =
celery_ast.args
|> Map.put(:sequence_name, sequence.name)
|> Map.put(:locals, %{celery_ast.args.locals | body: celery_ast.args.locals.body ++ regimen_params ++ farm_event_params})
celery_ast = %{celery_ast | args: celery_args}
FarmbotCeleryScript.schedule(celery_ast, at, sequence)
end
end