Current section

Files

Jump to
super_cache lib interface main.ex
Raw

lib/interface/main.ex

defmodule SuperCache do
@moduledoc """
SuperCache is an in-memory cache library built on top of ETS.
## Features
- Auto-scales partitions based on CPU scheduler count.
- Supports tuple and struct storage.
- Provides lazy (buffered) writes for higher throughput.
- Includes built-in Queue, Stack, KeyValue and Struct abstractions.
## Limitations
- Single-node only (no cluster replication in the current version).
- Supports tuple data natively; other types must be wrapped in a tuple.
## Quick start
SuperCache.start!()
# Push tuple to cache
SuperCache.put!({:my_key, "some value"})
# Get tuple by tuple style
SuperCache.get!({:my_key, nil}) # => [{:my_key, "some value"}]
# Get tuple by key/partition data.
SuperCache.get_by_key_partition!(:my_key)
SuperCache.stop()
"""
require Logger
alias SuperCache.{Bootstrap, Buffer, Config, Partition, Storage}
## Lifecycle ##
@doc "Start SuperCache with default config. Raises on error."
@spec start!() :: :ok
def start!(), do: Bootstrap.start!()
@doc "Start SuperCache with `opts`. Raises on error."
@spec start!(keyword) :: :ok
def start!(opts), do: Bootstrap.start!(opts)
@doc "Start SuperCache with default config. Returns `:ok | {:error, reason}`."
@spec start() :: :ok | {:error, Exception.t()}
def start(), do: safe(fn -> Bootstrap.start!() end)
@doc "Start SuperCache with `opts`. Returns `:ok | {:error, reason}`."
@spec start(keyword) :: :ok | {:error, Exception.t()}
def start(opts), do: safe(fn -> Bootstrap.start!(opts) end)
@doc "Returns `true` when SuperCache has been started."
@spec started?() :: boolean
def started?(), do: Config.get_config(:started, false)
@doc "Stop SuperCache and free all ETS memory."
@spec stop() :: :ok
def stop(), do: Bootstrap.stop()
## Write ##
@doc """
Store a tuple in the cache. Raises on error.
The tuple must be at least as large as `max(key_pos, partition_pos) + 1`.
"""
@spec put!(tuple) :: true
def put!(data) when is_tuple(data) do
partition = data |> Config.get_partition!() |> Partition.get_partition()
Logger.debug(fn -> "super_cache, put key=#{inspect(Config.get_key!(data))} partition=#{inspect(partition)}" end)
Storage.put(data, partition)
end
@doc "Store a tuple. Returns `true | {:error, reason}`."
@spec put(tuple) :: true | {:error, Exception.t()}
def put(data), do: safe(fn -> put!(data) end)
@doc """
Enqueue a tuple for buffered (lazy) write.
Use when immediate read-back is not required; improves write throughput.
"""
@spec lazy_put(tuple) :: :ok
def lazy_put(data) when is_tuple(data), do: Buffer.enqueue(data)
## Read ##
@doc """
Retrieve all records matching the key in `data`. Raises on error.
Returns a list of tuples (may be empty).
"""
@spec get!(tuple) :: [tuple]
def get!(data) when is_tuple(data) do
key = Config.get_key!(data)
partition = data |> Config.get_partition!() |> Partition.get_partition()
Logger.debug(fn -> "super_cache, get key=#{inspect(key)} partition=#{inspect(partition)}" end)
Storage.get(key, partition)
end
@doc "Retrieve records. Returns `[tuple] | {:error, reason}`."
@spec get(tuple) :: [tuple] | {:error, Exception.t()}
def get(data), do: safe(fn -> get!(data) end)
@doc "Retrieve records by explicit `key` and `partition` value."
@spec get_by_key_partition!(any, any) :: [tuple]
def get_by_key_partition!(key, partition_data) do
Storage.get(key, Partition.get_partition(partition_data))
end
@doc "Retrieve records where `key_pos == partition_pos` (same value for both)."
@spec get_same_key_partition!(any) :: [tuple]
def get_same_key_partition!(key), do: get_by_key_partition!(key, key)
@doc """
Retrieve records matching an ETS match pattern (`:ets.match/2`).
Pass `:_` as `partition_data` to scan all partitions.
"""
@spec get_by_match!(any, tuple) :: [[any]]
def get_by_match!(partition_data, pattern) when is_tuple(pattern) do
reduce_partitions(partition_data, [], fn p, acc ->
Storage.get_by_match(pattern, p) ++ acc
end)
end
@doc "Scan all partitions with an ETS match pattern."
@spec get_by_match!(tuple) :: [[any]]
def get_by_match!(pattern) when is_tuple(pattern), do: get_by_match!(:_, pattern)
@doc """
Retrieve full objects matching an ETS match-object pattern (`:ets.match_object/2`).
Pass `:_` as `partition_data` to scan all partitions.
"""
@spec get_by_match_object!(any, tuple) :: [tuple]
def get_by_match_object!(partition_data, pattern) when is_tuple(pattern) do
reduce_partitions(partition_data, [], fn p, acc ->
Storage.get_by_match_object(pattern, p) ++ acc
end)
end
@doc "Scan all partitions with an ETS match-object pattern."
@spec get_by_match_object!(tuple) :: [tuple]
def get_by_match_object!(pattern) when is_tuple(pattern), do: get_by_match_object!(:_, pattern)
## Scan ##
@doc """
Fold over all records in a partition (or all partitions when `:_`).
`fun/2` receives `(record, accumulator)` and must return the new accumulator.
"""
@spec scan!(any, (any, any -> any), any) :: any
def scan!(partition_data, fun, acc) when is_function(fun, 2) do
reduce_partitions(partition_data, acc, fn p, result ->
Storage.scan(fun, result, p)
end)
end
@doc "Fold over all partitions."
@spec scan!((any, any -> any), any) :: any
def scan!(fun, acc) when is_function(fun, 2), do: scan!(:_, fun, acc)
## Delete ##
@doc "Delete the record whose key and partition match those in `data`. Raises on error."
@spec delete!(tuple) :: :ok
def delete!(data) when is_tuple(data) do
key = Config.get_key!(data)
partition = data |> Config.get_partition!() |> Partition.get_partition()
Storage.delete(key, partition)
:ok
end
@doc "Delete all records from all partitions."
@spec delete_all() :: :ok
def delete_all() do
Partition.get_all_partition()
|> List.flatten()
|> Enum.each(&Storage.delete_all/1)
end
@doc """
Delete records matching `pattern` in a partition (or all partitions for `:_`).
"""
@spec delete_by_match!(any, tuple) :: :ok
def delete_by_match!(partition_data, pattern) when is_tuple(pattern) do
partition_data
|> resolve_partitions()
|> Enum.each(&Storage.delete_match(pattern, &1))
end
@doc "Delete matching records from all partitions."
@spec delete_by_match!(tuple) :: :ok
def delete_by_match!(pattern) when is_tuple(pattern), do: delete_by_match!(:_, pattern)
@doc "Delete by explicit `key` and `partition_data`."
@spec delete_by_key_partition!(any, any) :: true
def delete_by_key_partition!(key, partition_data) do
Storage.delete(key, Partition.get_partition(partition_data))
end
@doc "Delete where key and partition value are the same."
@spec delete_same_key_partition!(any) :: true
def delete_same_key_partition!(key), do: delete_by_key_partition!(key, key)
## Stats ##
@doc "Return `[{partition, size}]` for every partition plus a `total:` keyword."
@spec stats() :: keyword
def stats() do
entries =
Partition.get_all_partition()
|> List.flatten()
|> Enum.map(&Storage.stats/1)
total = Enum.reduce(entries, 0, fn {_, n}, acc -> acc + n end)
entries ++ [total: total]
end
## Private helpers ##
# Resolves partition_data to a flat list of ETS table names.
defp resolve_partitions(:_), do: List.flatten(Partition.get_all_partition())
defp resolve_partitions(data), do: [Partition.get_partition(data)]
# Reduce over resolved partitions, threading an accumulator.
defp reduce_partitions(partition_data, acc, fun) do
partition_data
|> resolve_partitions()
|> Enum.reduce(acc, fun)
end
# Wrap a raising function in a rescue, returning {:error, exception}.
defp safe(fun) do
fun.()
rescue
err ->
Logger.error(Exception.format(:error, err, __STACKTRACE__))
{:error, err}
end
end