The first-class trivial-cache surface (implementation.md §6.1) — a
partitioned, single-owner cache over the generic Fief.Vnode
behaviour and Fief.Router. The runtime half is Fief.Cache.VnodeImpl;
this module is caller-side: it hashes key to a vnode
(Fief.Hasher, shared with Fief.Key) and calls straight through
Fief.Router.call/4.
Single-owner, NOT replicated (Decision)
Each key lives on exactly one node's ETS table — the vnode it hashes to.
A get/3 for a key owned elsewhere is a real network hop through the
router, not a local read; there is no cross-node replication and no
local read-through fast path. What the fief ownership guarantees buy
instead is single-writer-per-key coherence: at most one node ever
serves a given key at a time (the planner's exclusivity guarantee,
design §5), so a put/3 and a concurrent get/3 for the same key can
never race across two nodes that both think they own it. That property
is what makes read-through caching safe without cross-node stale-write
races, and it is the foundation the (fast-follow) dogpile-prevention
loader will build on: only one loader for a given key can ever be in
flight, because only one agent owns that key's vnode at a time.
Drop-on-handoff (Decision)
Fief.Cache.VnodeImpl drops everything on both handoff_out (reports
drained immediately — nothing worth shipping) and handoff_in (starts
empty). A cache holds no durable truth by definition, so there is
nothing to preserve across ownership changes; the new owner simply
starts cold and repopulates from misses.
Hashing (Decision)
Fief.Cache shares Fief.Hasher with Fief.Key: the same pluggable
:hasher module (defaulting to Fief.Hasher.Default, :erlang.phash2/2
over binaries), never a fun — it is fingerprinted. Cross-node agreement on
the hasher module is enforced at join time via
Fief.Cache.VnodeImpl.config_fingerprint/1, mirroring
Fief.Key.VnodeImpl's fingerprint for the same reason: caller-side
hashing means a joiner computing a different vnode for the same key
would silently split the keyspace.
One cache per instance (Decision)
Unlike Fief.Key (whose {key_module, key} identity lets many key
modules share one instance's vnode space), Fief.Cache is anonymous:
an instance runs exactly one vnode_impl, so an instance configured
with vnode_impl: {Fief.Cache.VnodeImpl, opts} is the cache
namespace — there is no use Fief.Cache and no cache-module
multiplexing. This forecloses running Fief.Cache and Fief.Key in
the same instance: an instance's vnode space is either key-addressed
processes or cache entries, never both. Two caches (or a cache and a
key layer) require two instances.
Usage
# instance boot:
# {Fief, name: :my_instance, vnode_impl: {Fief.Cache.VnodeImpl, []},
# partitions: 1024, ...}
:ok = Fief.Cache.put(:my_instance, "user:42", %{name: "Ada"})
{:ok, %{name: "Ada"}} = Fief.Cache.get(:my_instance, "user:42")
:ok = Fief.Cache.delete(:my_instance, "user:42")
:error = Fief.Cache.get(:my_instance, "user:42")Fast-follow
v1 ships get/put/delete only. Planned follow-ups, and where they'll
land:
- TTL and eviction — per-entry expiry armed in
Fief.Cache.VnodeImpl'sFief.Vnode.init/2/ reaped viaFief.Vnode.handle_info/2(the same seam-timer patternFief.Key.VnodeImpluses for its sweep cadence); size bounds as:ets.new/2options alongside today's:set, :public. fetch/4with a loader — dispatched fromFief.Cache.VnodeImpl'sFief.Vnode.handle_message/3, spawning an impl-owned process to run the loader so a slow or hanging loader never blocks the agent. Single-owner placement is what makes dogpile prevention sound here: only one loader per key can ever be in flight.- Handoff support — shipping entries to the recipient on a planned
transfer (
Fief.Vnode.send_peer/2through the session channel, inFief.Cache.VnodeImpl'sFief.Vnode.handoff_out/2/Fief.Vnode.handoff_in/2) instead of dropping, so a rebalance warms the new owner rather than costing a cold start. Unplanned moves (node death, aborted transfers) necessarily stay cold — there is no donor to ship from.
Summary
Functions
Remove key from instance: :ok whether or not it was present, or the
Fief.Router.call/4 error shape on a routing failure. opts takes
:timeout. Raises ArgumentError for structural misuse, as put/4.
Fetch key from instance: {:ok, value} on a hit, :error on a miss
(the Map.fetch/2 shape), or the Fief.Router.call/4 error shape on a
routing failure. opts takes :timeout. Raises ArgumentError for
structural misuse, as put/4.
The presumed owner of key — hint-grade, like Fief.Router.owner_of/2.
Store value under key on instance: :ok, or the Fief.Router.call/4
error shape on a routing failure. opts takes :timeout (forwarded to
Fief.Router.call/4, covering the whole :moved-retry loop). Raises
ArgumentError for structural misuse (no vnode impl, a non-binary key the
instance's :hasher rejects) — those are caller bugs, not runtime conditions.
The vnode key maps to on instance, per the instance's immutable
hasher/partitions (read from the router config written once at instance
boot): rem(hasher.hash_key(key), partitions).
Types
@type key() :: term()
A user key: a binary, or any term the instance's :hasher accepts.
Functions
@spec delete(atom(), key(), keyword()) :: :ok | {:error, Fief.Router.call_error()}
Remove key from instance: :ok whether or not it was present, or the
Fief.Router.call/4 error shape on a routing failure. opts takes
:timeout. Raises ArgumentError for structural misuse, as put/4.
@spec get(atom(), key(), keyword()) :: {:ok, term()} | :error | {:error, Fief.Router.call_error()}
Fetch key from instance: {:ok, value} on a hit, :error on a miss
(the Map.fetch/2 shape), or the Fief.Router.call/4 error shape on a
routing failure. opts takes :timeout. Raises ArgumentError for
structural misuse, as put/4.
The presumed owner of key — hint-grade, like Fief.Router.owner_of/2.
@spec put(atom(), key(), term(), keyword()) :: :ok | {:error, Fief.Router.call_error()}
Store value under key on instance: :ok, or the Fief.Router.call/4
error shape on a routing failure. opts takes :timeout (forwarded to
Fief.Router.call/4, covering the whole :moved-retry loop). Raises
ArgumentError for structural misuse (no vnode impl, a non-binary key the
instance's :hasher rejects) — those are caller bugs, not runtime conditions.
@spec vnode!(atom(), key()) :: non_neg_integer()
The vnode key maps to on instance, per the instance's immutable
hasher/partitions (read from the router config written once at instance
boot): rem(hasher.hash_key(key), partitions).