kura_cache (kura_cache v0.0.1)
View SourceApplication-side read cache for kura projects.
Sending one chat message, serving one authenticated request, executing one webhook — hot paths read the same few rows over and over, and none of them change often. This module keeps those values in an ETS table owned by a supervised process and invalidates them on write.
The shape:
- one public ETS table (
read_concurrency), owned by thekura_cachegen_server so it outlives the request processes that fill it; fetch/3is the only read API: a hit returns the cached value, a miss runs the loader and caches whatever it returns — exceptundefinedand{error, _}, which pass through uncached (caching a transient failure turns a blip into a bug);- entries carry a per-kind TTL as a backstop: an eviction the application forgot to write costs staleness for seconds, not forever. A kind with no configured TTL is not cached at all — adding a kind is a deliberate act, so a stale read can always be traced to the configuration;
evict/2after every write to the underlying data. In a cluster the eviction must also reach the other nodes:kura_cachedoes not pick a transport, it calls the configuredbroadcastfun and exportslocal_evict/2/local_flush/1for the receiving side. Wire it to syn, pg, or nothing (single node).
The cache is not a source of truth and never serves writes — anything that must be correct under concurrency goes straight to the database.
Row-cache sugar
fetch_row/3 / evict_row/2 cache rows of a kura schema keyed by
primary key: the kind is the schema module, the key is normalized from
the schema's key fields (a bare value for single-column keys, a
#{field => value} map for composite ones — the same spec convention
as kura_repo_worker:get/3). The discipline is cache-aside, see
docs/cache-aside-guide.md: only read the cache by primary key, write
the database first, evict (never re-put) after writes.
Configuration
kura_cache:start_link(#{
kinds => #{
my_schema => 60_000, %% TTL in ms; kind = schema module or any atom
fanout => 30_000
},
broadcast => fun my_app:broadcast_eviction/1, %% optional
sweep_interval => 60_000 %% optional, default 60s
}).Telemetry
[kura_cache, hit], [kura_cache, miss] and [kura_cache, evict]
are executed with #{count => 1} and #{kind => Kind} metadata.
Summary
Functions
Drop everything, locally only.
Drop one entry locally and hand the event to the broadcast hook.
Evict the row addressed by the primary-key Spec.
Cached read. On a miss LoadFun is run and its value cached;
undefined and {error, _} values are returned but never cached.
A kind without a configured TTL is never cached, and a cache that is
not running behaves as all-miss — callers need no guard.
Cached row read, keyed by the schema's primary key. The kind is the
schema module (configure its TTL under that name); Spec is a bare
value for single-column keys or a #{field => value} map for
composite ones. LoadFun typically wraps the system-of-record read
Drop every entry of a kind, locally and via the broadcast hook.
Drop every cached row of Schema — the backstop after update_all,
delete_all or raw SQL, where the affected keys are unknown.
Local-only eviction: what a broadcast receiver calls.
Local-only flush of a kind: what a broadcast receiver calls.
Start the cache. kinds maps a kind to its TTL in milliseconds;
broadcast, when given, is called with a broadcast_event/0 after
every evict/2 and flush/1 — deliver it to the other nodes and call
local_evict/2 / local_flush/1 there.
Types
Functions
-spec clear() -> ok.
Drop everything, locally only.
Drop one entry locally and hand the event to the broadcast hook.
Evict the row addressed by the primary-key Spec.
Cached read. On a miss LoadFun is run and its value cached;
undefined and {error, _} values are returned but never cached.
A kind without a configured TTL is never cached, and a cache that is
not running behaves as all-miss — callers need no guard.
-spec fetch_row(module(), term(), fun(() -> {ok, map()} | {error, term()})) -> {ok, map()} | {error, term()}.
Cached row read, keyed by the schema's primary key. The kind is the
schema module (configure its TTL under that name); Spec is a bare
value for single-column keys or a #{field => value} map for
composite ones. LoadFun typically wraps the system-of-record read:
kura_cache:fetch_row(user_schema, Id, fun() ->
kura_repo_worker:get(db_repo, user_schema, Id)
end)
-spec flush(kind()) -> ok.
Drop every entry of a kind, locally and via the broadcast hook.
-spec flush_rows(module()) -> ok.
Drop every cached row of Schema — the backstop after update_all,
delete_all or raw SQL, where the affected keys are unknown.
Local-only eviction: what a broadcast receiver calls.
-spec local_flush(kind()) -> ok.
Local-only flush of a kind: what a broadcast receiver calls.
Start the cache. kinds maps a kind to its TTL in milliseconds;
broadcast, when given, is called with a broadcast_event/0 after
every evict/2 and flush/1 — deliver it to the other nodes and call
local_evict/2 / local_flush/1 there.
-spec stats() -> #{size => non_neg_integer(), memory_words => non_neg_integer()}.