The single pluggable seam for the key-addressed surfaces — Fief.Key and
Fief.Cache — both of which do caller-side key→vnode hashing over the
generic Fief.Vnode/Fief.Router layer and must compute the identical
mapping to agree with each other and across nodes. A hasher answers one
question: how does a key term map to an integer used for vnode assignment?
Living here (rather than under Fief.Key) keeps Fief.Cache from depending
on the key layer for something that is really neutral substrate.
The seam (Decision, implementation.md §7)
A hasher is a module implementing this behaviour — never a fun — because
the module atom is the fingerprinted term (config_fingerprint/1 returns
{:hasher, module}). Versioning is by module identity: a future algorithm is
a new module, and every node pins the module at join. The default is
Fief.Hasher.Default (:erlang.phash2/2 over binaries).
Fief computes the vnode as rem(hasher.hash_key(key), partitions) via
vnode!/4, the shared substrate both surfaces call.
What a custom hasher signs up for
hash_key/1 receives the raw key term, so key identity becomes whatever
the hasher makes of the term's structure. The fingerprint pins the module
identity across nodes, but behavioral stability over time is your contract —
the same register the old key-encoder docs carried. Pitfalls to encode
deliberately against:
1and1.0are different terms; a charlist and a binary are different terms — decide which your keyspace treats as equal.- A struct key changes shape when your app adds a field, so a rolling deploy can have two versions hashing the "same" key to different vnodes — a keyspace split with no framework signal. Version the hasher (a new module) when the term shape changes, exactly as you would repartition.
- Encode injectively and deterministically: two nodes disagreeing on
hash_key/1output for a key is the double-ownership hazard the fingerprint gates against, but a hasher whose output drifts within one module name is undetectable by fief.
Summary
Callbacks
Map a raw key term to a non-negative integer. Called caller-side on every
node, so it must be deterministic and stable across nodes and OTP releases
(design §9). Fief reduces the result modulo partitions.
Functions
Resolve and validate the :hasher impl opt into the join fingerprint
{:hasher, module} (defaulting to Fief.Hasher.Default). Raises
ArgumentError (prefixed with who, the impl module's name) if the hasher is
not a module implementing Fief.Hasher — a fun is rejected because the hasher
is fingerprinted and a fingerprint must be a stable, cross-node-comparable
term. This is the shared substrate both first-class vnode impls'
config_fingerprint/1 call.
The vnode key maps to under hasher for a partitions-wide keyspace:
rem(hasher.hash_key(key), partitions). Validates the hasher's return (must
be a non-negative integer) and prefixes any ArgumentError — the hasher's
own (e.g. the default hasher rejecting a non-binary key) or the return-shape
check — with who, the calling surface's name ("Fief.Key" / "Fief.Cache"),
so each surface's error names itself. This is the shared substrate both
key-addressed surfaces hash through.
Callbacks
@callback hash_key(key :: term()) :: non_neg_integer()
Map a raw key term to a non-negative integer. Called caller-side on every
node, so it must be deterministic and stable across nodes and OTP releases
(design §9). Fief reduces the result modulo partitions.
Functions
Resolve and validate the :hasher impl opt into the join fingerprint
{:hasher, module} (defaulting to Fief.Hasher.Default). Raises
ArgumentError (prefixed with who, the impl module's name) if the hasher is
not a module implementing Fief.Hasher — a fun is rejected because the hasher
is fingerprinted and a fingerprint must be a stable, cross-node-comparable
term. This is the shared substrate both first-class vnode impls'
config_fingerprint/1 call.
@spec vnode!(module(), term(), pos_integer(), String.t()) :: non_neg_integer()
The vnode key maps to under hasher for a partitions-wide keyspace:
rem(hasher.hash_key(key), partitions). Validates the hasher's return (must
be a non-negative integer) and prefixes any ArgumentError — the hasher's
own (e.g. the default hasher rejecting a non-binary key) or the return-shape
check — with who, the calling surface's name ("Fief.Key" / "Fief.Cache"),
so each surface's error names itself. This is the shared substrate both
key-addressed surfaces hash through.