Current section
Files
Jump to
Current section
Files
lib/fief.ex
defmodule Fief do
@moduledoc """
Consistent vnode ownership for Elixir clusters.
Fief is instance-based: nothing runs under the `:fief` application itself.
Start instances in your own supervision tree, Ecto/Oban-style:
children = [
{Fief,
name: MyApp.Fief,
authority: {Fief.Authority.Local, []},
partitions: 64}
]
The instance `:name` is the **namespace** — the identity of the coordination
domain, which must match across every node participating in this instance.
Multiple instances coexist in one BEAM and on one database.
See `docs/design.md` for guarantees and protocols, `docs/implementation.md`
for the code shape this module tree follows.
"""
@doc """
Optional instance-module sugar (implementation.md §9, deferred from M4 to
M6): `defmodule MyApp.Fief do use Fief, partitions: 64, ... end` generates a
module-named instance — `child_spec/1` (runtime opts override use-time
opts; `:name` is always the module) plus `call/3`, `cast/2`, `owner_of/1`,
`table/0`, `epoch/0`, `leader/0` wrapping the instance-first API. The core
stays plain opts; this is convenience only.
"""
defmacro __using__(use_opts) do
quote do
@doc false
def child_spec(opts) do
unquote(use_opts)
|> Keyword.merge(opts)
|> Keyword.put(:name, __MODULE__)
|> Fief.child_spec()
end
@doc "Start this instance. Prefer `{#{inspect(__MODULE__)}, opts}` in a supervision tree."
def start_link(opts \\ []) do
Fief.start_link(
unquote(use_opts)
|> Keyword.merge(opts)
|> Keyword.put(:name, __MODULE__)
)
end
@doc "See `Fief.call/4`."
def call(vnode, msg, opts \\ []), do: Fief.call(__MODULE__, vnode, msg, opts)
@doc "See `Fief.cast/3`."
def cast(vnode, msg), do: Fief.cast(__MODULE__, vnode, msg)
@doc "See `Fief.owner_of/2`."
def owner_of(vnode), do: Fief.owner_of(__MODULE__, vnode)
@doc "See `Fief.table/1`."
def table, do: Fief.table(__MODULE__)
@doc "See `Fief.epoch/1`."
def epoch, do: Fief.epoch(__MODULE__)
@doc "See `Fief.leader/1`."
def leader, do: Fief.leader(__MODULE__)
end
end
@doc false
def child_spec(opts) do
name = Keyword.fetch!(opts, :name)
%{
id: {Fief, name},
start: {Fief.Supervisor, :start_link, [opts]},
type: :supervisor
}
end
@doc "Start a Fief instance. Prefer `{Fief, opts}` in a supervision tree."
@spec start_link(keyword()) :: Supervisor.on_start()
def start_link(opts), do: Fief.Supervisor.start_link(opts)
# -- the instance-first API surface (implementation.md §1) --------------------
#
# Vnode-level, key-free: keys (hashing, `Fief.call(key, msg)`) arrive with
# `Fief.Key` at M6, layered on exactly these functions.
@doc """
Call `vnode` of `instance` with `msg` and await the impl's reply:
`{:ok, reply}` or `{:error, reason}`. Runs in the caller (no router
process); `opts` takes `:timeout` (ms), covering the entire `:moved`-retry
loop. See `Fief.Router.call/4`.
"""
@spec call(atom(), non_neg_integer(), term(), keyword()) ::
{:ok, term()} | {:error, Fief.Router.call_error()}
def call(instance, vnode, msg, opts \\ []), do: Fief.Router.call(instance, vnode, msg, opts)
@doc "Fire-and-forget send to `vnode`'s owner. See `Fief.Router.cast/3`."
@spec cast(atom(), non_neg_integer(), term()) :: :ok | {:error, Fief.Router.call_error()}
def cast(instance, vnode, msg), do: Fief.Router.cast(instance, vnode, msg)
@doc "The presumed owner of `vnode` — hint-grade. See `Fief.Router.owner_of/2`."
@spec owner_of(atom(), non_neg_integer()) ::
{:ok, term()} | {:error, :no_owner | :unreachable | :not_running}
def owner_of(instance, vnode), do: Fief.Router.owner_of(instance, vnode)
@doc """
The cached vnode table — `{:ok, %{vnode => {owner, prev_owner, row_epoch}},
epoch}` — hint-grade, from this node's routing cache (an ETS read; the
Authority is the truth).
"""
@spec table(atom()) :: {:ok, map(), non_neg_integer()} | {:error, :not_ready | :not_running}
def table(instance) do
tab = Fief.Router.Cache.table(instance)
case :ets.lookup(tab, :table_epoch) do
[{:table_epoch, epoch}] ->
rows =
:ets.select(tab, [
{{{:route, :"$1"}, :"$2", :"$3", :"$4"}, [], [{{:"$1", {{:"$2", :"$3", :"$4"}}}}]}
])
{:ok, Map.new(rows), epoch}
[] ->
{:error, :not_ready}
end
rescue
ArgumentError -> {:error, :not_running}
end
@doc "The epoch the cached table was read at. Hint-grade, an ETS read."
@spec epoch(atom()) :: {:ok, non_neg_integer()} | {:error, :not_ready | :not_running}
def epoch(instance) do
case :ets.lookup(Fief.Router.Cache.table(instance), :table_epoch) do
[{:table_epoch, epoch}] -> {:ok, epoch}
[] -> {:error, :not_ready}
end
rescue
ArgumentError -> {:error, :not_running}
end
@doc """
The current planner leader, `{:ok, {node_id, term}}`, answered from the
Authority through the instance's Leadership adapter. `{:error, :disabled}`
when the instance runs no leadership (`leadership: false` or no vnode impl).
"""
@spec leader(atom()) ::
{:ok, {term(), pos_integer()}}
| {:error, :none | :unreachable | :disabled | :not_running}
def leader(instance) do
with {:ok, cfg} <- Fief.Router.config(instance) do
case cfg.leadership do
nil -> {:error, :disabled}
{mod, name} -> mod.leader(name)
end
end
end
end