defmodule ActiveMemory.TableHeir do @moduledoc """ Stable "owner of last resort" for the ETS tables created by `ActiveMemory.Store`. Each ETS table is created with this process set as its ETS `:heir`. When a store process terminates, ETS transfers the table to this process instead of destroying it, preserving the data. When the store restarts it reclaims the table via `claim/1`, so ETS data survives store crashes and restarts. This process is started automatically by `ActiveMemory.Application`; no changes to a host application's supervision tree and no changes to the `Store` API are required. When the heir is not running, stores fall back to the previous behaviour of creating a fresh table. """ use GenServer @name __MODULE__ def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: @name) end @doc """ Reclaim ownership of `table` for the calling process. Returns `:ok` when the heir is currently holding `table` and has handed it back to the caller, or `{:error, :not_held}` when the heir is not the table's owner (the table does not exist, or it is owned by another process). iex> ActiveMemory.TableHeir.claim(:a_table_the_heir_does_not_hold) {:error, :not_held} """ @spec claim(atom()) :: :ok | {:error, :not_held} def claim(table) do GenServer.call(@name, {:claim, table, self()}) end @doc """ Return the pid of the running heir, or `nil` when it is not started. """ @spec whereis() :: pid() | nil def whereis, do: Process.whereis(@name) @impl true def init(_opts), do: {:ok, MapSet.new()} @impl true def handle_call({:claim, table, pid}, _from, held_tables) do case held?(table) do true -> :ets.give_away(table, pid, table) {:reply, :ok, MapSet.delete(held_tables, table)} false -> {:reply, {:error, :not_held}, held_tables} end end @impl true def handle_info({:"ETS-TRANSFER", _table_ref, _from, table}, held_tables) do {:noreply, MapSet.put(held_tables, table)} end def handle_info(_message, held_tables), do: {:noreply, held_tables} defp held?(table) do case :ets.whereis(table) do :undefined -> false _table_ref -> :ets.info(table, :owner) == self() end end end