Fief.Cache (Fief v0.1.0)

Copy Markdown View Source

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:

Summary

Types

A user key: a binary, or any term the instance's :hasher accepts.

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

key()

@type key() :: term()

A user key: a binary, or any term the instance's :hasher accepts.

Functions

delete(instance, key, opts \\ [])

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

get(instance, key, opts \\ [])

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

owner_of(instance, key)

@spec owner_of(atom(), key()) ::
  {:ok, term()} | {:error, :no_owner | :unreachable | :not_running}

The presumed owner of key — hint-grade, like Fief.Router.owner_of/2.

put(instance, key, value, opts \\ [])

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

vnode!(instance, key)

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