Fief.Hasher behaviour (Fief v0.1.0)

Copy Markdown View Source

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:

  • 1 and 1.0 are 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/1 output 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

hash_key(key)

@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

fingerprint!(opts, who)

@spec fingerprint!(
  keyword(),
  String.t()
) :: {:hasher, module()}

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.

vnode!(hasher, key, partitions, who)

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