Packages

A distributed cache library for Elixir with pluggable backends, topologies, and near-cache support.

Current section

Files

Jump to
vela_cache lib vela backend ets table_owner.ex
Raw

lib/vela/backend/ets/table_owner.ex

defmodule Vela.Backend.ETS.TableOwner do
@moduledoc """
Owns the ETS tables for a cache instance.
ETS tables are destroyed when their owner process dies. By keeping
a dedicated, crash-resistant process as the owner (and heir), we
ensure the tables survive even if the backend process crashes.
"""
use GenServer
# ---- Public API ----
def start_link(opts) do
name = Keyword.fetch!(opts, :name)
GenServer.start_link(__MODULE__, opts, name: table_owner_name(name))
end
@doc "Returns the name of the data ETS table for a cache."
def data_table(cache_name), do: :"vela_data_#{cache_name}"
@doc "Returns the name of the TTL index ETS table for a cache."
def ttl_table(cache_name), do: :"vela_ttl_#{cache_name}"
# ---- GenServer Callbacks ----
@impl true
def init(opts) do
name = Keyword.fetch!(opts, :name)
data_table = data_table(name)
ttl_table = ttl_table(name)
# :set → each key is unique (like a hash map)
# :public → any process can read/write (no bottleneck)
# read_concurrency → optimises for many concurrent readers
# write_concurrency → uses internal striping to allow concurrent writes
# named_table → accessible by name, not just the reference
:ets.new(data_table, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
# :ordered_set → entries are sorted by key
# We key TTL entries as {expires_at, key} so a range scan
# from {0, nil} to {now, nil} finds all expired entries cheaply.
:ets.new(ttl_table, [
:ordered_set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
# Set ourselves as the heir of both tables.
# If we give the tables to another process and THAT process dies,
# the tables come back to us automatically.
:ets.setopts(data_table, {:heir, self(), :data_table})
:ets.setopts(ttl_table, {:heir, self(), :ttl_table})
{:ok, %{name: name, data_table: data_table, ttl_table: ttl_table}}
end
# Called by the VM when a table is transferred to us (heir recovery)
@impl true
def handle_info({:"ETS-TRANSFER", table, _from_pid, _data}, state) do
# The backend crashed and we got the table back.
# When the backend restarts, it will call give_table/2 to reclaim it.
{:noreply, Map.put(state, :recovered_table, table)}
end
@doc """
Called by the backend after it starts up to take ownership of the tables.
"""
def give_tables(cache_name, new_owner_pid) do
owner = table_owner_name(cache_name)
GenServer.call(owner, {:give_tables, new_owner_pid})
end
@impl true
def handle_call({:give_tables, new_owner_pid}, _from, state) do
:ets.give_away(state.data_table, new_owner_pid, :data_table)
:ets.give_away(state.ttl_table, new_owner_pid, :ttl_table)
{:reply, :ok, state}
end
defp table_owner_name(cache_name), do: :"vela_table_owner_#{cache_name}"
end