Packages

Graph-first runtime for building agent systems on the BEAM in Elixir

Current section

Files

Jump to
ex_ai lib ex_ai persistence mnesia.ex
Raw

lib/ex_ai/persistence/mnesia.ex

defmodule ExAI.Persistence.Mnesia do
@compile {:no_warn_undefined, :mnesia}
@moduledoc """
Mnesia bootstrap and table management helpers for ExAI persistence.
"""
alias ExAI.Error
alias ExAI.Persistence.Schema
@default_wait_timeout 15_000
@spec bootstrap(keyword()) :: :ok | {:error, Error.t()}
def bootstrap(opts \\ []) do
with :ok <- maybe_set_dir(Keyword.get(opts, :dir)),
:ok <- ensure_schema(Keyword.get(opts, :nodes, [node()])),
:ok <- ensure_started(),
:ok <- ensure_tables(opts),
:ok <- wait_for_tables(Keyword.get(opts, :wait_timeout, @default_wait_timeout)) do
:ok
end
end
@spec reset(keyword()) :: :ok | {:error, Error.t()}
def reset(opts \\ []) do
nodes = Keyword.get(opts, :nodes, [node()])
with :ok <- maybe_set_dir(Keyword.get(opts, :dir)),
:ok <- stop(),
:ok <- delete_schema(nodes),
:ok <- ensure_schema(nodes),
:ok <- ensure_started(),
:ok <- ensure_tables(opts),
:ok <- wait_for_tables(Keyword.get(opts, :wait_timeout, @default_wait_timeout)) do
:ok
end
end
@spec ensure_schema([node()]) :: :ok | {:error, Error.t()}
def ensure_schema(nodes \\ [node()]) do
case :mnesia.create_schema(nodes) do
:ok ->
:ok
{:error, reason} when reason != :badarg ->
if already_exists?(reason) do
:ok
else
{:error,
Error.new(:internal, "failed to create mnesia schema",
details: %{reason: inspect(reason)}
)}
end
{:error, :badarg} ->
{:error,
Error.new(:internal, "failed to create mnesia schema", details: %{reason: :badarg})}
end
end
@spec ensure_started() :: :ok | {:error, Error.t()}
def ensure_started do
case :mnesia.start() do
:ok ->
:ok
{:error, reason} ->
{:error,
Error.new(:internal, "failed to start mnesia", details: %{reason: inspect(reason)})}
end
end
@spec stop() :: :ok | {:error, Error.t()}
def stop do
case :mnesia.stop() do
:stopped ->
:ok
{:error, {:not_started, :mnesia}} ->
:ok
{:error, reason} ->
{:error,
Error.new(:internal, "failed to stop mnesia", details: %{reason: inspect(reason)})}
end
end
@spec delete_schema([node()]) :: :ok | {:error, Error.t()}
def delete_schema(nodes \\ [node()]) do
case :mnesia.delete_schema(nodes) do
:ok ->
:ok
{:error, reason} ->
if no_exists?(reason) do
:ok
else
{:error,
Error.new(:internal, "failed to delete mnesia schema",
details: %{reason: inspect(reason)}
)}
end
end
end
@spec ensure_tables(keyword()) :: :ok | {:error, Error.t()}
def ensure_tables(opts \\ []) do
Enum.reduce_while(Schema.tables(), :ok, fn table, _acc ->
table_opts = Schema.table_options(table, opts)
case :mnesia.create_table(table, table_opts) do
{:atomic, :ok} ->
{:cont, :ok}
{:aborted, {:already_exists, ^table}} ->
{:cont, :ok}
{:aborted, reason} ->
{:halt,
{:error,
Error.new(:internal, "failed to create mnesia table",
details: %{table: table, reason: inspect(reason)}
)}}
end
end)
end
@spec wait_for_tables(timeout()) :: :ok | {:error, Error.t()}
def wait_for_tables(timeout_ms \\ @default_wait_timeout) do
case :mnesia.wait_for_tables(Schema.tables(), timeout_ms) do
:ok ->
:ok
{:timeout, pending_tables} ->
{:error,
Error.new(:timeout, "timed out waiting for mnesia tables",
details: %{pending_tables: pending_tables, timeout_ms: timeout_ms}
)}
{:error, reason} ->
{:error,
Error.new(:internal, "failed while waiting for mnesia tables",
details: %{reason: inspect(reason)}
)}
end
end
@spec maybe_set_dir(String.t() | nil) :: :ok
defp maybe_set_dir(nil), do: :ok
defp maybe_set_dir(dir) when is_binary(dir) do
Application.put_env(:mnesia, :dir, String.to_charlist(dir))
:ok
end
@spec already_exists?(term()) :: boolean()
defp already_exists?({_, {:already_exists, _}}), do: true
defp already_exists?({:already_exists, _}), do: true
defp already_exists?(_), do: false
@spec no_exists?(term()) :: boolean()
defp no_exists?({_, {:no_exists, _}}), do: true
defp no_exists?(_), do: false
end