lib/openagents/box/run.ex

58e6347eeb72 · 3 KB

defmodule OpenAgents.Box.Run do
  @moduledoc "One durable detached command run on a Box computer."

  use Ecto.Schema
  import Ecto.Changeset

  @states ~w(admitted dispatched running completed failed cancelled timed_out lost)
  @terminal_states ~w(completed failed cancelled timed_out lost)
  @maximum_command_bytes 8_000
  @maximum_idempotency_key_bytes 256

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime_usec]

  schema "box_runs" do
    belongs_to :conversation, OpenAgents.Conversations.Conversation
    belongs_to :conversation_box, OpenAgents.Box.ConversationBox
    field :requesting_principal, :map
    field :command, :string
    field :idempotency_key, :string
    field :state, :string, default: "admitted"
    field :exit_status, :integer
    field :timed_out, :boolean, default: false
    field :output, :string, default: ""
    field :output_base_offset, :integer, default: 0
    field :last_output_offset, :integer, default: 0
    field :pid, :integer
    field :run_directory, :string
    field :failure_reason, :string
    field :dispatch_attempted_at, :utc_datetime_usec
    field :probe_attempted_at, :utc_datetime_usec
    field :admitted_at, :utc_datetime_usec
    field :dispatched_at, :utc_datetime_usec
    field :started_at, :utc_datetime_usec
    field :finished_at, :utc_datetime_usec
    field :cancellation_requested_at, :utc_datetime_usec
    field :cancellation_effective_at, :utc_datetime_usec
    field :deadline_at, :utc_datetime_usec
    timestamps()
  end

  @type t :: %__MODULE__{}

  @spec states() :: [String.t()]
  def states, do: @states

  @spec terminal_states() :: [String.t()]
  def terminal_states, do: @terminal_states

  @spec terminal?(t()) :: boolean()
  def terminal?(%__MODULE__{state: state}), do: state in @terminal_states

  @spec maximum_command_bytes() :: pos_integer()
  def maximum_command_bytes, do: @maximum_command_bytes

  @spec maximum_idempotency_key_bytes() :: pos_integer()
  def maximum_idempotency_key_bytes, do: @maximum_idempotency_key_bytes

  @spec changeset(t(), map()) :: Ecto.Changeset.t()
  def changeset(run, attributes) do
    run
    |> cast(attributes, [
      :requesting_principal,
      :command,
      :idempotency_key,
      :state,
      :exit_status,
      :timed_out,
      :output,
      :output_base_offset,
      :last_output_offset,
      :pid,
      :run_directory,
      :failure_reason,
      :dispatch_attempted_at,
      :probe_attempted_at,
      :admitted_at,
      :dispatched_at,
      :started_at,
      :finished_at,
      :cancellation_requested_at,
      :cancellation_effective_at,
      :deadline_at
    ])
    |> put_programmatic(attributes, :conversation_id)
    |> put_programmatic(attributes, :conversation_box_id)
    |> validate_required([
      :conversation_id,
      :conversation_box_id,
      :requesting_principal,
      :command,
      :idempotency_key,
      :run_directory,
      :admitted_at,
      :deadline_at
    ])
    |> validate_inclusion(:state, @states)
    |> validate_length(:command, max: @maximum_command_bytes)
    |> validate_length(:idempotency_key, max: @maximum_idempotency_key_bytes)
    |> validate_number(:output_base_offset, greater_than_or_equal_to: 0)
    |> validate_number(:last_output_offset, greater_than_or_equal_to: 0)
    |> foreign_key_constraint(:conversation_id)
    |> foreign_key_constraint(:conversation_box_id)
    |> unique_constraint(:idempotency_key)
    |> unique_constraint(:conversation_box_id, name: :box_runs_one_active_per_box_index)
  end

  defp put_programmatic(changeset, attributes, field) do
    case Map.fetch(attributes, field) do
      {:ok, value} -> put_change(changeset, field, value)
      :error -> changeset
    end
  end
end