Packages
vela_cache
0.1.0
A distributed cache library for Elixir with pluggable backends, topologies, and near-cache support.
Current section
Files
Jump to
Current section
Files
lib/vela/backend/ets.ex
defmodule Vela.Backend.ETS do
@moduledoc """
ETS-backed cache backend.
Reads and writes go directly to ETS tables (bypassing any GenServer
mailbox) for maximum throughput. The tables are owned and protected
by Vela.Backend.ETS.TableOwner.
"""
@behaviour Vela.Backend
alias Vela.Backend.ETS.TableOwner
alias Vela.Cache.Entry
# State for this backend — just the table names
defstruct [:data_table, :ttl_table]
@impl true
def init(config) do
state = %__MODULE__{
data_table: TableOwner.data_table(config.name),
ttl_table: TableOwner.ttl_table(config.name)
}
{:ok, state}
end
@impl true
def get(%__MODULE__{data_table: table}, key) do
case :ets.lookup(table, key) do
[{^key, entry}] -> {:ok, entry}
[] -> {:error, :not_found}
end
end
@impl true
def put(%__MODULE__{data_table: data_table, ttl_table: ttl_table} = state, %Entry{} = entry) do
# Clean up old TTL entry if overwriting an existing key
case :ets.lookup(data_table, entry.key) do
[{_key, old_entry}] ->
unless old_entry.expires_at == :infinity do
:ets.delete(ttl_table, {old_entry.expires_at, entry.key})
end
[] ->
:ok
end
# Store the entry in the main data table
:ets.insert(data_table, {entry.key, entry})
# Register the expiry in the TTL index table
# Key format: {expires_at, key} — ordered_set sorts by this,
# so a range scan finds all expired entries efficiently
unless entry.expires_at == :infinity do
:ets.insert(ttl_table, {{entry.expires_at, entry.key}, entry.key})
end
{:ok, state}
end
@impl true
def delete(%__MODULE__{data_table: data_table, ttl_table: ttl_table} = state, key) do
# Look up the entry first so we can remove it from the TTL index
case :ets.lookup(data_table, key) do
[{^key, entry}] ->
:ets.delete(data_table, key)
unless entry.expires_at == :infinity do
:ets.delete(ttl_table, {entry.expires_at, key})
end
[] ->
:ok
end
{:ok, state}
end
@impl true
def get_many(%__MODULE__{data_table: table}, keys) do
result =
Enum.reduce(keys, %{}, fn key, acc ->
case :ets.lookup(table, key) do
[{^key, entry}] -> Map.put(acc, key, entry)
[] -> acc
end
end)
{:ok, result}
end
@impl true
def put_many(state, entries) do
new_state =
Enum.reduce(entries, state, fn entry, acc_state ->
{:ok, next_state} = put(acc_state, entry)
next_state
end)
{:ok, new_state}
end
@impl true
def flush(%__MODULE__{data_table: data_table, ttl_table: ttl_table} = state) do
:ets.delete_all_objects(data_table)
:ets.delete_all_objects(ttl_table)
{:ok, state}
end
@impl true
def flush_expired(%__MODULE__{data_table: data_table, ttl_table: ttl_table} = state, now) do
# TTL table rows are {{expires_at, key}, key}
# Match the nested tuple to extract expires_at for the guard
match_spec = [
{
# pattern: {{expires_at, key}, key}
{{:"$1", :"$2"}, :"$3"},
# guard: expires_at <= now
[{:"=<", :"$1", now}],
# action: delete it
[true]
}
]
# Collect expired keys so we can delete them from the data table too
expired_keys =
:ets.select(ttl_table, [
{
{{:"$1", :"$2"}, :"$3"},
[{:"=<", :"$1", now}],
# return the data key
[:"$3"]
}
])
count = length(expired_keys)
# Delete from both tables
:ets.select_delete(ttl_table, match_spec)
Enum.each(expired_keys, &:ets.delete(data_table, &1))
{:ok, count, state}
end
@impl true
def size(%__MODULE__{data_table: table}) do
:ets.info(table, :size)
end
@impl true
def delete_by_tag(%__MODULE__{data_table: data_table, ttl_table: ttl_table} = state, tag) do
# Scan data table for entries whose tags list contains the given tag.
# ETS match spec: the value is an Entry struct stored as {key, entry}.
# Entry has a :tags field — we select entries where tag is in the list.
matching =
:ets.foldl(
fn {key, entry}, acc ->
if tag in entry.tags, do: [{key, entry} | acc], else: acc
end,
[],
data_table
)
# Delete each matching entry from both tables
Enum.each(matching, fn {key, entry} ->
:ets.delete(data_table, key)
unless entry.expires_at == :infinity do
:ets.delete(ttl_table, {entry.expires_at, key})
end
end)
{:ok, length(matching), state}
end
end