Packages
kura_cache
0.0.1
Application-side 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
Current section
Files
Jump to
Current section
Files
kura_cache
README.md
README.md
# kura_cache
Application-side read cache for [kura](https://github.com/Taure/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
```erlang
%% 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`:
```erlang
%% 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
1. Only read the cache by primary key; list/conditional queries go
straight to the database.
2. Write the database first; touch the cache only after the write
succeeded.
3. Evict after writes — never re-put (re-putting races concurrent
writers; eviction's worst case is one extra miss).
4. After `update_all` / `delete_all` / raw SQL, `flush_rows/1` the
schema: those paths cannot tell you which keys changed.