Vnode-level send/:moved-retry (design §5.4, implementation.md §5). Pure
module — no process: call/4 and cast/3 run in the caller. Key
hashing does not exist here; a vnode-level user routes by vnode id directly,
and Fief.Key (M6) hashes above this.
The call loop
Resolve the owner (ETS cache; Authority read_table on a miss), mint the
destination name via Fief.Seam.vnode_name/2, send a Fief.Wire.msg/4
envelope with from = {self(), ref}, and selectively receive {ref, reply}.
A reply decoding as a :moved envelope (receivers answer it on the same
path as user replies; Fief.Wire.decode/1 distinguishes) patches the local
routing hint and retries — up to max_moved_hops bounces — then falls back
to one authoritative read_table after a jittered pause (Fief.Seam,
the anti-stampede of design §5.4), then gives up with {:error, :exhausted}.
Timeout discipline
One Fief.Seam.send_after/2 seam timer covers the entire loop — every
hop, the jitter pause, and the fallback — never per hop (receive-after is
banned in lib; the wait is a selective receive on the reply ref, the same
ref's :moved, or the deadline timer). Silent drops — fenced or killed
owners produce no reply, by design — surface here as {:error, :timeout}.
The timer is cancelled through Fief.Seam.cancel_and_flush/1 on every exit
path, covering the cancel-after-due stale delivery (M3 convention).
Because the loop runs in arbitrary caller processes, it emits the :handled
checkpoint after processing each received message: under simulation a
scheduler-stepped delivery to a caller blocked in this loop settles like any
other delivery; outside simulation (and for direct, unscripted replies) the
checkpoint is a no-op.
Single-writer rule
Fief.Node is the only writer of the route ETS table. This module NEVER
writes it: :moved deltas live in per-call local state, and are handed up
to Fief.Node as an async notification ({:fief_moved_delta, delta}) —
a refresh trigger, closing phase A's TODO. Fief.Node re-reads the
Authority; the delta is never written verbatim (hints accelerate, never
authorize).
{:error, :noconnect} from Transport (no dist connection — never a
blocking auto-connect) triggers the same refresh notification, then one
authoritative re-resolve and resend; a second :noconnect is returned to
the caller.
Summary
Functions
Call vnode with msg, awaiting the impl's reply: {:ok, reply} or
{:error, reason}. Options: :timeout (ms, whole-loop, default
5000).
Fire-and-forget send to vnode's owner. :ok means handed to the
transport, never delivered (silent-drop semantics are the contract).
The presumed owner of vnode — hint-grade (design §8): the cached row, or
one authoritative read on a cache miss.
Types
Functions
@spec call(atom(), non_neg_integer(), term(), keyword()) :: {:ok, term()} | {:error, call_error()}
Call vnode with msg, awaiting the impl's reply: {:ok, reply} or
{:error, reason}. Options: :timeout (ms, whole-loop, default
5000).
@spec cast(atom(), non_neg_integer(), term()) :: :ok | {:error, call_error()}
Fire-and-forget send to vnode's owner. :ok means handed to the
transport, never delivered (silent-drop semantics are the contract).
@spec owner_of(atom(), non_neg_integer()) :: {:ok, term()} | {:error, :no_owner | :unreachable | :not_running}
The presumed owner of vnode — hint-grade (design §8): the cached row, or
one authoritative read on a cache miss.