Fief.Seam behaviour (Fief v0.1.0)

Copy Markdown View Source

The determinism seam (implementation.md §10). Everything nondeterministic — time, randomness, cross-node communication, and sim-visible progress marks — goes through this one module, so a test harness can take it over. Together the seams support a protocol-level deterministic simulation harness (FoundationDB/TigerBeetle-style): one scheduler owning one event queue.

Compile-switched: prod builds bind Fief.Seam.Real with zero indirection; test builds bind Fief.Seam.Sim, which falls through to Real unless the calling process has hooked a simulation (Fief.Sim). One compile key, config :fief, :seam, selects the implementation for the function seams; the checkpoint/2 macro is a separate boolean switch (:fief, :trace_checkpoints).

The function seams stay real function calls dispatched through a bound @impl_mod, not macro-expanded raw calls — deliberately: send/3, send_after/2, and the rest remain, on Fief.Seam.Real, single traceable prod choke points, so every cross-node send and every timer arm is one MFA you can :dbg/recon-trace.

Time

Every clock read and every deadline in fief goes through this module: lease basis, fence timer, planner cadence, pull timeouts, sweep ticks, poll intervals. Raw timers (Process.send_after, :erlang.start_timer, gen_statem state timeouts, receive-after) are banned outside the seam modules and enforced by scripts/check_seams.sh.

Timer semantics follow :erlang.start_timer/3: send_after/2 arms a timer that delivers {:timeout, ref, msg} to the calling process, where ref is the returned reference. All times are milliseconds.

Randomness

Every jitter and random choice goes through this module: :moved-fallback jitter, leader-poll jitter. Direct :rand. calls outside the seam modules are banned and enforced by scripts/check_seams.sh. The sim implementation is seedable per-process for deterministic tests and falls through to real otherwise.

Transport

vnode-name minting and the dist send (implementation.md §5). Every cross-node send goes through here; the real implementation is :erlang.send({name, node}, msg, [:noconnect]) — a :noconnect return is a routing failure for the caller to handle, never a blocking auto-connect.

Inbound addressing uses per-vnode registered names namespaced by instance — a bounded atom set (instances × partitions), minted at instance boot.

Checkpoints

The scheduler controls what crosses its queue — seam timers, transport deliveries, arbiter ops — but intra-node execution between those points is invisible to it. checkpoint/2 marks such points: places where the harness needs to observe progress it cannot infer from the queue, the load-bearing case being quiescence ("this delivery has been fully processed, virtual time may advance").

Fief.Seam.checkpoint(:renewed, node: node_id)

Unlike the function seams this is a boolean compile switch, not a module binding: in prod builds the macro expands to :ok — no call, no argument evaluation, nothing to keep off the hot path. In test builds it expands to an emit through Fief.Seam.Sim, which sends {:fief_checkpoint, tag, meta, pid} to the tracer bound to the calling process (Fief.Sim.bind_tracer/1) and no-ops when unhooked — real-unless- hooked, like every other seam.

Because prod builds discard arguments unevaluated, checkpoint args must be cheap and side-effect-free: literal tags, existing bindings. Never compute something only for a checkpoint.

Summary

Functions

Cancel a timer and consume its {:timeout, ref, msg} if it already fired into the caller's mailbox — the cancel-after-due stale-delivery case for code that runs in arbitrary caller processes (Fief.Router), where the gen_statem stale-timer catch-all convention (M3) is not available and a leaked timeout message would pollute the caller's mailbox. Non-blocking: a fired-but-not-yet-delivered timer (possible under simulation, where a due timer is a pending scheduler event) is simply not there to flush, matching :erlang.cancel_timer/1 semantics.

Cancel a timer armed with send_after/2. Returns remaining ms or false.

Mark a sim-visible progress point (see the "Checkpoints" section above). In prod builds this expands to :ok with tag/meta discarded unevaluated; in test builds it emits through Fief.Seam.Sim.emit/2.

Monotonic milliseconds. The only clock fief code may read.

Mint the registered name of instance's planner channel — the cross-node address settle reports are relayed to (M4 phase B). One atom per instance; the leading node's Fief.Planner registers it while leading, so a send to {planner_name, leader_node} reaches the planner or drops silently like any other unregistered name.

Send msg to the process registered as name on node, without connecting. {:error, :noconnect} means no live dist connection — a routing failure to refresh-and-retry, per implementation.md §5. A send to an unregistered name (local or remote) is silently dropped, exactly like dist: :ok means handed off, never delivered — non-delivery surfaces as the caller's reply timeout.

Arm a timer delivering {:timeout, ref, msg} to self() after ms.

Uniform float in [0.0, 1.0).

Uniform integer in 1..n.

Mint the registered name for vnode_id of instance. Bounded atom set.

Types

timer_ref()

@type timer_ref() :: reference()

Callbacks

cancel_timer(timer_ref)

@callback cancel_timer(timer_ref()) :: non_neg_integer() | false

monotonic_time()

@callback monotonic_time() :: integer()

planner_name(instance)

@callback planner_name(instance :: atom()) :: atom()

send(name, node, msg)

@callback send(name :: atom(), node :: node(), msg :: term()) ::
  :ok | {:error, :noconnect | :nosuspend}

send_after(ms, msg)

@callback send_after(ms :: non_neg_integer(), msg :: term()) :: timer_ref()

uniform()

@callback uniform() :: float()

uniform(n)

@callback uniform(n :: pos_integer()) :: pos_integer()

vnode_name(instance, vnode_id)

@callback vnode_name(instance :: atom(), vnode_id :: non_neg_integer()) :: atom()

Functions

cancel_and_flush(ref)

@spec cancel_and_flush(timer_ref()) :: :ok

Cancel a timer and consume its {:timeout, ref, msg} if it already fired into the caller's mailbox — the cancel-after-due stale-delivery case for code that runs in arbitrary caller processes (Fief.Router), where the gen_statem stale-timer catch-all convention (M3) is not available and a leaked timeout message would pollute the caller's mailbox. Non-blocking: a fired-but-not-yet-delivered timer (possible under simulation, where a due timer is a pending scheduler event) is simply not there to flush, matching :erlang.cancel_timer/1 semantics.

cancel_timer(ref)

@spec cancel_timer(timer_ref()) :: non_neg_integer() | false

Cancel a timer armed with send_after/2. Returns remaining ms or false.

checkpoint(tag, meta \\ [])

(macro)

Mark a sim-visible progress point (see the "Checkpoints" section above). In prod builds this expands to :ok with tag/meta discarded unevaluated; in test builds it emits through Fief.Seam.Sim.emit/2.

monotonic_time()

@spec monotonic_time() :: integer()

Monotonic milliseconds. The only clock fief code may read.

planner_name(instance)

@spec planner_name(atom()) :: atom()

Mint the registered name of instance's planner channel — the cross-node address settle reports are relayed to (M4 phase B). One atom per instance; the leading node's Fief.Planner registers it while leading, so a send to {planner_name, leader_node} reaches the planner or drops silently like any other unregistered name.

send(name, node, msg)

@spec send(atom(), node(), term()) :: :ok | {:error, :noconnect | :nosuspend}

Send msg to the process registered as name on node, without connecting. {:error, :noconnect} means no live dist connection — a routing failure to refresh-and-retry, per implementation.md §5. A send to an unregistered name (local or remote) is silently dropped, exactly like dist: :ok means handed off, never delivered — non-delivery surfaces as the caller's reply timeout.

send_after(ms, msg)

@spec send_after(non_neg_integer(), term()) :: timer_ref()

Arm a timer delivering {:timeout, ref, msg} to self() after ms.

uniform()

@spec uniform() :: float()

Uniform float in [0.0, 1.0).

uniform(n)

@spec uniform(pos_integer()) :: pos_integer()

Uniform integer in 1..n.

vnode_name(instance, vnode_id)

@spec vnode_name(atom(), non_neg_integer()) :: atom()

Mint the registered name for vnode_id of instance. Bounded atom set.