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/distributed/main.ex
defmodule SuperCache.Distributed do
@moduledoc """
Drop-in replacements for `SuperCache.put!/1`, `get!/1`, and the delete
family that are cluster-aware.
Import or alias this module when running in cluster mode instead of
calling `SuperCache` directly, or update `SuperCache` to delegate here
when `:cluster` config is `:distributed`.
"""
require Logger
alias SuperCache.{Config, Partition, Storage}
alias SuperCache.Cluster.{Manager, Replicator}
## Write ##
@doc "Store a tuple on the local node and replicate to peer replicas."
@spec put!(tuple) :: true
def put!(data) when is_tuple(data) do
partition_data = Config.get_partition!(data)
partition_idx = Partition.get_pattition_order(partition_data)
partition = Partition.get_partition(partition_data)
result = Storage.put(data, partition)
Replicator.replicate(partition_idx, :put, data)
result
end
@spec put(tuple) :: true | {:error, Exception.t()}
def put(data), do: safe(fn -> put!(data) end)
@doc """
Retrieve records by key.
Options:
- `read_mode: :local` (default) — read from local ETS immediately.
- `read_mode: :primary` — if this node is not the primary, forward the
read to the primary node for strong consistency.
"""
@spec get!(tuple, keyword) :: [tuple]
def get!(data, opts \\ []) when is_tuple(data) do
key = Config.get_key!(data)
partition_data = Config.get_partition!(data)
partition_idx = Partition.get_pattition_order(partition_data)
partition = Partition.get_partition(partition_data)
case Keyword.get(opts, :read_mode, :local) do
:local ->
Storage.get(key, partition)
:primary ->
{primary, _} = Manager.get_replicas(partition_idx)
if primary == node() do
Storage.get(key, partition)
else
:erpc.call(primary, Storage, :get, [key, partition], 5_000)
end
end
end
@spec get(tuple, keyword) :: [tuple] | {:error, Exception.t()}
def get(data, opts \\ []), do: safe(fn -> get!(data, opts) end)
## Delete ##
@spec delete!(tuple) :: :ok
def delete!(data) when is_tuple(data) do
key = Config.get_key!(data)
partition_data = Config.get_partition!(data)
partition_idx = Partition.get_pattition_order(partition_data)
partition = Partition.get_partition(partition_data)
Storage.delete(key, partition)
Replicator.replicate(partition_idx, :delete, key)
:ok
end
@spec delete_all() :: :ok
def delete_all() do
Partition.get_all_partition()
|> List.flatten()
|> Enum.with_index()
|> Enum.each(fn {p, idx} ->
Storage.delete_all(p)
Replicator.replicate(idx, :delete_all)
end)
end
@spec delete_by_match!(any, tuple) :: :ok
def delete_by_match!(partition_data, pattern) when is_tuple(pattern) do
partitions =
case partition_data do
:_ -> List.flatten(Partition.get_all_partition()) |> Enum.with_index()
d -> [{Partition.get_partition(d), Partition.get_pattition_order(d)}]
end
Enum.each(partitions, fn {p, idx} ->
Storage.delete_match(pattern, p)
Replicator.replicate(idx, :delete_match, pattern)
end)
end
## Private ##
defp safe(fun) do
fun.()
rescue
err ->
Logger.error(Exception.format(:error, err, __STACKTRACE__))
{:error, err}
end
end
## ─────────────────────────────────────────────────────────────
## SuperCache.Distributed (now delegates entirely to Router)
## ─────────────────────────────────────────────────────────────
defmodule SuperCache.Distributed do
@moduledoc """
Cluster-aware public API for SuperCache.
Every write is routed to the partition's primary node automatically.
Reads default to local ETS for lowest latency; pass `read_mode: :primary`
or `read_mode: :quorum` when consistency matters.
## Read modes
| Mode | Latency | Consistency |
|------------|----------|---------------------|
| `:local` | ~0 μs | Eventual |
| `:primary` | 1 hop | Strong (linearizable per key) |
| `:quorum` | 1 hop | Majority vote |
"""
require Logger
alias SuperCache.Cluster.Router
## Write ##
@doc "Store a tuple. Routes to primary if needed. Raises on error."
@spec put!(tuple) :: true
def put!(data) when is_tuple(data), do: Router.route_put!(data)
@doc "Store a tuple. Returns `true | {:error, reason}`."
@spec put(tuple) :: true | {:error, Exception.t()}
def put(data), do: safe(fn -> Router.route_put!(data) end)
## Read ##
@doc """
Retrieve records matching the key in `data`.
# Eventual read (default, fastest)
Distributed.get!({:user, 1, nil})
# Strong read — forwarded to primary if needed
Distributed.get!({:user, 1, nil}, read_mode: :primary)
# Quorum read — majority of replicas must agree
Distributed.get!({:user, 1, nil}, read_mode: :quorum)
"""
@spec get!(tuple, keyword) :: [tuple]
def get!(data, opts \\ []) when is_tuple(data), do: Router.route_get!(data, opts)
@doc "Retrieve records. Returns `[tuple] | {:error, reason}`."
@spec get(tuple, keyword) :: [tuple] | {:error, Exception.t()}
def get(data, opts \\ []), do: safe(fn -> Router.route_get!(data, opts) end)
## Delete ##
@doc "Delete the record whose key/partition match `data`. Routes to primary."
@spec delete!(tuple) :: :ok
def delete!(data) when is_tuple(data), do: Router.route_delete!(data)
@doc "Delete all records across all partitions (routed per-partition)."
@spec delete_all() :: :ok
def delete_all(), do: Router.route_delete_all()
@doc "Delete records matching `pattern` in a partition (or all with `:_`)."
@spec delete_by_match!(any, tuple) :: :ok
def delete_by_match!(partition_data, pattern) when is_tuple(pattern) do
Router.route_delete_match!(partition_data, pattern)
end
@doc "Delete by explicit key and partition value."
@spec delete_by_key_partition!(any, any) :: :ok
def delete_by_key_partition!(key, partition_data) do
Router.route_delete_by_key_partition!(key, partition_data)
end
## Private ##
defp safe(fun) do
fun.()
rescue
err ->
Logger.error(Exception.format(:error, err, __STACKTRACE__))
{:error, err}
end
end