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/partition/partition_api.ex
defmodule SuperCache.Partition do
@moduledoc """
Partition resolution and management for SuperCache.
SuperCache splits data across multiple ETS tables (partitions) to reduce
contention and improve parallelism. This module handles:
- **Hashing** — mapping arbitrary terms to partition indices using `:erlang.phash2/2`.
- **Resolution** — translating partition indices to ETS table atoms.
- **Lifecycle** — initialising and tearing down the partition registry.
## How partitioning works
1. A term (usually extracted from a tuple via `:partition_pos`) is hashed
with `:erlang.phash2/2` modulo the number of partitions.
2. The resulting index is looked up in `SuperCache.Partition.Holder` to
get the corresponding ETS table atom.
3. All reads and writes for that term go to that table.
## Example
# Start with 4 partitions
SuperCache.Partition.start(4)
# Resolve partition for a term
SuperCache.Partition.get_partition(:user_123)
# => :"SuperCache.Storage.Ets_2"
# Get partition index directly
SuperCache.Partition.get_partition_order(:user_123)
# => 2
# Clean up
SuperCache.Partition.stop()
"""
require Logger
require SuperCache.Log
alias SuperCache.Partition.Holder
# Cache num_partition in persistent_term for O(1) reads on the hot path.
@pt_num_partition {__MODULE__, :num_partition}
# Cache partition table atoms in a tuple for O(1) indexed access.
# Eliminates the ETS lookup in Holder.get/1 on every partition resolution.
@pt_partitions {__MODULE__, :partition_tables}
# ─── Partition resolution ───────────────────────────────────────────────────
@doc """
Resolve the ETS table atom for a given term.
The term is hashed to derive a partition index, which is then looked up
in the `Holder` registry to return the corresponding ETS table name.
This is the primary entry point used by the public API when the caller
has a raw key or partition value.
## Examples
SuperCache.Partition.get_partition(:session_abc)
# => :"SuperCache.Storage.Ets_1"
SuperCache.Partition.get_partition({:user, 42})
# => :"SuperCache.Storage.Ets_3"
"""
@spec get_partition(any) :: atom
def get_partition(data) do
data
|> get_partition_order()
|> get_partition_by_idx()
end
@doc """
Resolve the ETS table atom directly from a partition index.
Does **not** re-hash the input. Safe to call from the Replicator and
Router where the index is already known.
## Examples
SuperCache.Partition.get_partition_by_idx(0)
# => :"SuperCache.Storage.Ets_0"
"""
@spec get_partition_by_idx(non_neg_integer) :: atom | nil
def get_partition_by_idx(idx) when is_integer(idx) and idx >= 0 do
fast_get_partition_by_idx(idx)
end
# Fast path: read partition table atom from cached tuple in persistent_term.
# Falls back to ETS lookup if cache not yet built (early boot).
# Returns `nil` if the partition index is not registered.
@compile {:inline, fast_get_partition_by_idx: 1}
defp fast_get_partition_by_idx(idx) do
case :persistent_term.get(@pt_partitions, nil) do
nil ->
# Cache not ready — fall back to ETS lookup.
Holder.get(idx)
tuple when is_tuple(tuple) and idx < tuple_size(tuple) ->
elem(tuple, idx)
_tuple ->
# Index out of bounds — fall back to ETS lookup.
Holder.get(idx)
end
end
# ─── Index helpers ──────────────────────────────────────────────────────────
@doc """
Compute the partition index for any term.
Uses `:erlang.phash2/2` with the configured number of partitions as the
range. The result is an integer in `0..(num_partitions - 1)`.
## Examples
SuperCache.Partition.get_partition_order(:hello)
# => 2
"""
@spec get_partition_order(any) :: non_neg_integer
@compile {:inline, get_partition_order: 1}
def get_partition_order(term) do
:erlang.phash2(term, fast_num_partition())
end
@doc """
Compute the partition index for a data tuple using the configured `:partition_pos`.
Extracts the element at `:partition_pos` from the tuple, then hashes it
to determine the partition index.
## Examples
SuperCache.Config.set_config(:partition_pos, 1)
SuperCache.Partition.get_partition_order_from_data({:user, 42, "data"})
# => (hash of 42) mod num_partitions
"""
@spec get_partition_order_from_data(tuple) :: non_neg_integer
def get_partition_order_from_data(data) when is_tuple(data) do
data
|> SuperCache.Config.get_partition!()
|> get_partition_order()
end
# ─── Cluster / management ───────────────────────────────────────────────────
@doc """
List all registered partition ETS table atoms.
Returns a flat list of table names, excluding internal registry entries.
## Examples
SuperCache.Partition.start(3)
SuperCache.Partition.get_all_partition()
# => [:"SuperCache.Storage.Ets_0", :"SuperCache.Storage.Ets_1", :"SuperCache.Storage.Ets_2"]
"""
@spec get_all_partition() :: [atom]
def get_all_partition(), do: Holder.get_all()
@doc """
Initialise the partition registry.
Stores the total partition count and registers each index (0 to N-1)
with its corresponding ETS table name.
Called by `SuperCache.Bootstrap` during startup.
## Examples
SuperCache.Partition.start(4)
# => :ok
"""
@spec start(pos_integer) :: :ok
def start(num_partition) when is_integer(num_partition) do
SuperCache.Log.debug(fn ->
"super_cache, partition, initialising #{num_partition} partition(s)"
end)
# Cache num_partition in persistent_term for fast hot-path reads.
:persistent_term.put(@pt_num_partition, num_partition)
Holder.set_num_partition(num_partition)
# Build partition table name tuple for O(1) indexed access.
prefix = SuperCache.Config.get_config(:table_prefix, "SuperCache.Storage.Ets")
partition_tuple =
0..(num_partition - 1)
|> Enum.map(fn order ->
table_name = String.to_atom("#{prefix}_#{order}")
Holder.set(order)
table_name
end)
|> List.to_tuple()
# Cache the tuple in persistent_term — safe because atoms are built at startup only.
:persistent_term.put(@pt_partitions, partition_tuple)
Logger.info("super_cache, partition, #{num_partition} partition(s) registered")
:ok
end
@doc """
Clear all partition registrations from the registry.
Does **not** delete the ETS tables themselves — only removes the
index-to-name mappings from the `Holder`.
Called by `SuperCache.Bootstrap` during shutdown.
"""
@spec stop() :: :ok
def stop() do
SuperCache.Log.debug(fn -> "super_cache, partition, clearing registry" end)
:persistent_term.erase(@pt_num_partition)
:persistent_term.erase(@pt_partitions)
Holder.clean()
:ok
end
@doc """
Return the configured number of partitions.
Reads directly from the `Holder` ETS table — no GenServer hop.
Raises if the partition count has not been initialised.
## Examples
SuperCache.Partition.start(8)
SuperCache.Partition.get_num_partition()
# => 8
"""
@spec get_num_partition() :: pos_integer
def get_num_partition() do
case :ets.lookup(Holder, :num_partition) do
[{:num_partition, :config, num}] when is_integer(num) and num > 0 ->
num
[] ->
raise "partition count not initialised. Call Partition.start/1 first."
other ->
raise "unexpected partition count entry: #{inspect(other)}"
end
end
# Fast path: read num_partition from persistent_term (set during start/1).
# Falls back to ETS lookup if not yet cached (early boot).
@compile {:inline, fast_num_partition: 0}
defp fast_num_partition() do
case :persistent_term.get(@pt_num_partition, :__not_set__) do
:__not_set__ ->
n = get_num_partition()
:persistent_term.put(@pt_num_partition, n)
n
n ->
n
end
end
@doc """
Return the number of online schedulers in the Erlang VM.
Used as the default partition count when `:num_partition` is not
explicitly configured.
## Examples
SuperCache.Partition.get_schedulers()
# => 8 (on an 8-core machine)
"""
@spec get_schedulers() :: pos_integer
def get_schedulers(), do: System.schedulers_online()
# ─── Deprecated / backwards compatibility ───────────────────────────────────
@doc false
@deprecated "Use get_partition_order/1 instead"
@spec get_hash(any) :: non_neg_integer
def get_hash(term), do: :erlang.phash2(term)
@doc false
@deprecated "Use get_partition_order/1 instead"
@spec get_partition_order(any) :: non_neg_integer
def get_pattition_order(term), do: get_partition_order(term)
end