Fief.Router (Fief v0.1.0)

Copy Markdown View Source

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

call_error()

@type call_error() ::
  :timeout
  | :exhausted
  | :no_owner
  | :noconnect
  | :nosuspend
  | :unreachable
  | :not_running

Functions

call(instance, vnode, msg, opts \\ [])

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

cast(instance, vnode, msg)

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

owner_of(instance, vnode)

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