Packages
An in-memory caching library using tuples as the core data type, with support for structs, key/value pairs, queues, and stacks. Includes experimental distributed caching.
Current section
Files
Jump to
Current section
Files
lib/app/config.ex
defmodule SuperCache.Config do
@moduledoc """
Central configuration store for SuperCache.
Acts as a GenServer-backed key-value store with an optimisation for
hot-path keys: values for `@fast_keys` are also written to `:persistent_term`
so reads are allocation-free (no GenServer hop).
## Fast keys
The following keys are served from `:persistent_term` on every read:
- `:key_pos` — tuple index used as the ETS key
- `:partition_pos` — tuple index used to select partition
- `:num_partition` — number of ETS partitions
- `:table_type` — ETS table type (`:set`, `:bag`, etc.)
- `:table_prefix` — prefix for ETS table atom names
All other keys fall back to a `GenServer.call/3` with a 5-second timeout.
## Example
SuperCache.Config.set_config(:my_key, "value")
SuperCache.Config.get_config(:my_key)
# => "value"
# Fast key — zero-cost read
SuperCache.Config.get_config(:num_partition)
# => 8
"""
use GenServer, restart: :permanent, shutdown: 5_000
require Logger
require SuperCache.Log
defexception message: "incorrect config"
alias __MODULE__
# Keys promoted to persistent_term for zero-cost reads on the hot path.
# Stored as a plain map so `is_map_key/2` works in guard clauses.
# `:cluster` is included here because `distributed?()` is called on every
# API operation and must avoid the GenServer hop.
@fast_keys %{
cluster: true,
key_pos: true,
partition_pos: true,
num_partition: true,
table_type: true,
table_prefix: true
}
## API ##
@doc """
Starts the Config GenServer.
Accepts a keyword list of initial configuration values.
"""
@spec start_link(keyword) :: :ignore | {:error, any} | {:ok, pid}
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Extract the key element from a data tuple using the configured `:key_pos`.
Raises `Config` if the tuple is too small.
## Examples
SuperCache.Config.set_config(:key_pos, 0)
SuperCache.Config.get_key!({:user, 1, "Alice"})
# => :user
"""
@spec get_key!(tuple) :: any
def get_key!(data) when is_tuple(data) do
pos = fast_get(:key_pos)
if pos < tuple_size(data) do
elem(data, pos)
else
raise Config,
message:
"tuple size (#{tuple_size(data)}) is lower than key_pos (#{pos}) for data: #{inspect(data)}"
end
end
@doc """
Extract the partition element from a data tuple using the configured `:partition_pos`.
Raises `Config` if the tuple is too small.
## Examples
SuperCache.Config.set_config(:partition_pos, 1)
SuperCache.Config.get_partition!({:user, 1, "Alice"})
# => 1
"""
@spec get_partition!(tuple) :: any
def get_partition!(data) when is_tuple(data) do
pos = fast_get(:partition_pos)
if pos < tuple_size(data) do
elem(data, pos)
else
raise Config,
message:
"tuple size (#{tuple_size(data)}) is lower than partition_pos (#{pos}) for data: #{inspect(data)}"
end
end
@doc """
Read a configuration value.
Hot keys (`@fast_keys`) are served from `:persistent_term` with no
GenServer hop. All other keys use `GenServer.call/3` with a 5-second
timeout.
## Examples
SuperCache.Config.get_config(:num_partition)
# => 8
SuperCache.Config.get_config(:unknown_key, :default)
# => :default
"""
@spec get_config(any, any) :: any
def get_config(key, default \\ nil) do
if is_map_key(@fast_keys, key) do
case :persistent_term.get({__MODULE__, key}, :__not_set__) do
:__not_set__ -> default
value -> value
end
else
GenServer.call(__MODULE__, {:get, key, default}, 5_000)
end
end
@doc """
Check whether a configuration key exists in the GenServer state.
Note: This always hits the GenServer, even for fast keys, because
`:persistent_term` does not distinguish between "not set" and "set to nil".
"""
@spec has_config?(any) :: boolean
def has_config?(key) do
GenServer.call(__MODULE__, {:has_config, key}, 5_000)
end
@doc """
Returns `true` when SuperCache is running in distributed mode.
Zero-cost read from `:persistent_term` — no GenServer hop.
This function is inlined by the compiler and is the fastest way to
check cluster mode on the hot path (called by every API operation).
## Examples
SuperCache.Config.distributed?()
# => true (when cluster: :distributed was set at startup)
"""
@spec distributed?() :: boolean
@compile {:inline, distributed?: 0}
def distributed? do
:persistent_term.get({__MODULE__, :cluster}, :local) == :distributed
end
@doc """
Store a configuration value.
Hot keys are also written to `:persistent_term` for fast subsequent reads.
"""
@spec set_config(any, any) :: :ok
def set_config(key, value) do
GenServer.call(__MODULE__, {:set, key, value}, 5_000)
end
@doc """
Delete a configuration value from the GenServer state and `:persistent_term`.
Uses a synchronous call to ensure the deletion is complete before returning,
preventing race conditions with concurrent reads.
"""
@spec delete_config(any) :: :ok
def delete_config(key) do
GenServer.call(__MODULE__, {:delete, key}, 5_000)
end
@doc """
Clear all configuration values and erase all `:persistent_term` entries
for hot keys.
"""
@spec clear_config() :: :ok
def clear_config() do
GenServer.call(__MODULE__, :clear, 5_000)
end
## GenServer callbacks ##
@impl true
def init(opts) do
SuperCache.Log.debug(fn ->
"super_cache, config, starting with opts: #{inspect(opts)}"
end)
state =
Enum.reduce(opts, %{}, fn {k, v}, acc -> Map.put(acc, k, v) end)
|> Map.put_new(:table_prefix, "SuperCache.Storage.Ets")
# Promote initial values to persistent_term.
Enum.each(state, fn {k, v} -> maybe_write_fast(k, v) end)
Logger.info("super_cache, config, started with #{map_size(state)} key(s)")
{:ok, state}
end
@impl true
def handle_call({:get, key, default}, _from, state) do
{:reply, Map.get(state, key, default), state}
end
def handle_call({:has_config, key}, _from, state) do
{:reply, Map.has_key?(state, key), state}
end
def handle_call({:set, key, value}, _from, state) do
maybe_write_fast(key, value)
SuperCache.Log.debug(fn -> "super_cache, config, set #{inspect(key)} = #{inspect(value)}" end)
{:reply, :ok, Map.put(state, key, value)}
end
def handle_call({:delete, key}, _from, state) do
:persistent_term.erase({__MODULE__, key})
SuperCache.Log.debug(fn -> "super_cache, config, deleted #{inspect(key)}" end)
{:reply, :ok, Map.delete(state, key)}
end
def handle_call(:clear, _from, state) do
_count = map_size(state)
Enum.each(Map.keys(@fast_keys), fn k -> :persistent_term.erase({__MODULE__, k}) end)
SuperCache.Log.debug(fn -> "super_cache, config, cleared #{_count} key(s)" end)
{:reply, :ok, %{}}
end
@impl true
def handle_cast(_msg, state) do
{:noreply, state}
end
@impl true
def terminate(reason, state) do
count = if state, do: map_size(state), else: 0
Logger.info(
"super_cache, config, shutting down (#{count} key(s) in state, reason: #{inspect(reason)})"
)
:ok
end
## Private helpers ##
# Bypasses GenServer — safe because persistent_term is read-optimised and
# updates are rare (only during start!/stop!).
@compile {:inline, fast_get: 1}
defp fast_get(key) do
:persistent_term.get({__MODULE__, key})
end
defp maybe_write_fast(key, value) when is_map_key(@fast_keys, key) do
:persistent_term.put({__MODULE__, key}, value)
end
defp maybe_write_fast(_key, _value), do: :ok
end