kura_cache
View SourceApplication-side read cache for kura projects: keyed memoization with per-kind TTL, row-cache sugar keyed by schema primary keys, telemetry, and a pluggable invalidation broadcast hook for clusters.
This is deliberately not a kura backend. Backends
(kura_sqlite, kura_postgres, kura_ets) implement storage;
caching is application policy — which reads are hot, how stale is
acceptable, how evictions reach other nodes — and lives above the
repo, next to the code that owns the write paths.
Usage
%% under your supervisor
kura_cache:start_link(#{
kinds => #{
user_schema => 60_000, %% row kinds: TTL per schema module
fanout => 30_000 %% free-form kinds work too
},
%% optional: deliver evictions to your other nodes (syn, pg, ...)
broadcast => fun my_app_cache:broadcast/1
}).
%% cached row read
get_user(Id) ->
kura_cache:fetch_row(user_schema, Id, fun() ->
kura_repo_worker:get(db_repo, user_schema, Id)
end).
%% after every write to the row: database first, then evict
ok = kura_cache:evict_row(user_schema, Id).
%% free-form memoization with the same TTL/eviction machinery
member_ids(ChannelId) ->
kura_cache:fetch(fanout, ChannelId, fun() -> load_member_ids(ChannelId) end).Values meaning "nothing there" (undefined, {error, _}) are returned
but never cached. A kind with no configured TTL is not cached at all. A
cache that is not running behaves as all-miss, so tests and boot need
no guards.
Cluster invalidation
kura_cache does not pick a transport. Configure broadcast; on the
receiving side call kura_cache:local_evict/2 / local_flush/1:
%% sender side (configured hook)
broadcast(Event) -> syn:publish(my_scope, cache_group, {cache, Event}).
%% receiver side (any process joined to the group)
handle_info({cache, {evict, Kind, Key}}, S) ->
kura_cache:local_evict(Kind, Key), {noreply, S};
handle_info({cache, {flush, Kind}}, S) ->
kura_cache:local_flush(Kind), {noreply, S}.Telemetry
[kura_cache, hit], [kura_cache, miss], [kura_cache, evict] with
#{count => 1} and #{kind => Kind} metadata.
Discipline
- Only read the cache by primary key; list/conditional queries go straight to the database.
- Write the database first; touch the cache only after the write succeeded.
- Evict after writes — never re-put (re-putting races concurrent writers; eviction's worst case is one extra miss).
- After
update_all/delete_all/ raw SQL,flush_rows/1the schema: those paths cannot tell you which keys changed.