kura_cache (kura_cache v0.0.1)

View Source

Application-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 the kura_cache gen_server so it outlives the request processes that fill it;
  • fetch/3 is the only read API: a hit returns the cached value, a miss runs the loader and caches whatever it returns — except undefined and {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/2 after every write to the underlying data. In a cluster the eviction must also reach the other nodes: kura_cache does not pick a transport, it calls the configured broadcast fun and exports local_evict/2 / local_flush/1 for 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

broadcast_event()

-type broadcast_event() :: {evict, kind(), term()} | {flush, kind()}.

kind()

-type kind() :: atom().

Functions

clear()

-spec clear() -> ok.

Drop everything, locally only.

evict(Kind, Key)

-spec evict(kind(), term()) -> ok.

Drop one entry locally and hand the event to the broadcast hook.

evict_row(Schema, Spec)

-spec evict_row(module(), term()) -> ok.

Evict the row addressed by the primary-key Spec.

fetch(Kind, Key, LoadFun)

-spec fetch(kind(), term(), fun(() -> term())) -> term().

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.

fetch_row(Schema, Spec, LoadFun)

-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)

flush(Kind)

-spec flush(kind()) -> ok.

Drop every entry of a kind, locally and via the broadcast hook.

flush_rows(Schema)

-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.

handle_call(Req, From, State)

handle_cast(Msg, State)

handle_info/2

init(Opts)

local_evict(Kind, Key)

-spec local_evict(kind(), term()) -> ok.

Local-only eviction: what a broadcast receiver calls.

local_flush(Kind)

-spec local_flush(kind()) -> ok.

Local-only flush of a kind: what a broadcast receiver calls.

start_link(Opts)

-spec start_link(map()) -> {ok, pid()} | {error, term()}.

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.

stats()

-spec stats() -> #{size => non_neg_integer(), memory_words => non_neg_integer()}.

terminate(Reason, State)