Packages

A distributed cache library for Elixir with pluggable backends, topologies, and near-cache support.

Current section

Files

Jump to
vela_cache lib vela near_cache.ex
Raw

lib/vela/near_cache.ex

defmodule Vela.NearCache do
@moduledoc """
L1 read-through layer for Vela caches.
When enabled, adds a local ETS table in front of the real topology.
Hot keys are served from L1 (microseconds) instead of hitting the
topology (which may involve network hops for Redis, Partitioned, etc.).
L1 is ephemeral — if the tables are lost, reads fall through to the
topology and L1 warms back up naturally.
This is NOT a topology. It's a transparent middleware that sits between
the Cache API and whatever topology is configured.
"""
alias Vela.Backend.ETS.TableOwner
alias Vela.Cache.Entry
@doc "Creates the L1 ETS tables for a cache."
def create_tables(cache_name) do
l1_name = l1_name(cache_name)
data_table = TableOwner.data_table(l1_name)
ttl_table = TableOwner.ttl_table(l1_name)
:ets.new(data_table, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
:ets.new(ttl_table, [
:ordered_set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: true
])
:ok
end
@doc "Look up a key in L1. Returns `{:ok, entry}` or `:miss`."
def get(cache_name, key) do
table = TableOwner.data_table(l1_name(cache_name))
case :ets.lookup(table, key) do
[{^key, entry}] ->
if Entry.expired?(entry) do
:ets.delete(table, key)
:miss
else
{:ok, entry}
end
[] ->
:miss
end
end
@doc "Store an entry in L1 with a capped TTL."
def put(cache_name, %Entry{} = entry, l1_ttl) do
l1_entry = cap_ttl(entry, l1_ttl)
table = TableOwner.data_table(l1_name(cache_name))
:ets.insert(table, {l1_entry.key, l1_entry})
:ok
end
@doc "Delete a key from L1."
def delete(cache_name, key) do
table = TableOwner.data_table(l1_name(cache_name))
:ets.delete(table, key)
:ok
end
@doc "Flush all L1 entries."
def flush(cache_name) do
table = TableOwner.data_table(l1_name(cache_name))
:ets.delete_all_objects(table)
:ok
end
@doc "Delete all L1 entries with the given tag."
def invalidate_tag(cache_name, tag) do
table = TableOwner.data_table(l1_name(cache_name))
:ets.foldl(
fn {key, entry}, acc ->
if tag in entry.tags do
:ets.delete(table, key)
acc + 1
else
acc
end
end,
0,
table
)
end
defp cap_ttl(entry, l1_ttl) do
now = System.monotonic_time(:millisecond)
l1_expires = now + l1_ttl
new_expires =
case entry.expires_at do
:infinity -> l1_expires
t -> min(t, l1_expires)
end
%{entry | expires_at: new_expires}
end
defp l1_name(cache_name), do: :"#{cache_name}_l1"
end