Fief.Key.Limbo (Fief v0.1.0)

Copy Markdown View Source

The per-instance limbo supervision for fence-surviving key servers (design §8, implementation.md §7 fence modes): a DynamicSupervisor plus the Fief.Key.Limbo.Tracker that manages it, supervised at the instance root via Fief.Vnode.instance_children/1 (M7 Decision — previously an unlinked island). The root is rest_for_one and the limbo is its first child, so it survives crash-restarts of every kernel child (a crashed Fief.Node fences the node — exactly when :continue survivors must keep living) and dies only with the instance. The fence kill itself never reaches here: it tears down agent link trees under Fief.Vnode.Manager.

Servers whose key module declares on_fence: :continue | :callback live here from birth (Decision, M6): a running process cannot be re-parented out of a DynamicSupervisor, and a supervisor terminates its children gracefully even when its own parent is brutally killed — so detach-at-fence is unimplementable, and placement at start is the honest mechanism. The impl still holds the monitors and the key→pid map; normal freeze/retire semantics are unchanged (servers stop themselves inside extract_state/1 regardless of who supervises them).

What "marked for cleanup" means in v1:

  • entries are tracked per {key_id, vnode_id} and enumerable (entries/1) — fenced-but-alive :continue processes are findable;
  • a new impl incarnation for a vnode reaps that vnode's limbo entries at init/2 (reap/2) — the node re-owning the vnode kills the stale survivors before serving (the reconnect-cleanup semantics of design §8 at the moment they matter for double-liveness on one node);
  • instance shutdown takes the whole subtree down through the root supervisor — nothing leaks.

Crash containment (:one_for_all here): the tracker and the DynamicSupervisor share a fate — a crash of either restarts both into an empty, mutually consistent state. The tracker's entry map is load-bearing for safety: reap/2 is what stops a stale survivor from double-serving a key, so an amnesiac tracker beside living survivors would leave them unreapable — a double-serve hole. Coupling fates closes that hole with no machinery: the restart is the recovery. Survivors die with the bookkeeper, which is acceptable — :continue liveness is explicitly best-effort (it forfeits guarantee 1 for side effects), survivors are crash-equivalent, and reap/2 kills them unceremoniously anyway; a tracker crash is a minimal, should-never-happen event. The rejected alternative — restart only the tracker and rebuild its map by asking each survivor to identify itself — was itself unsafe: the identity-call deadline would execute a healthy survivor whose mailbox is merely flooded (the canonical :continue case, a busy PubSub subscriber), so it never reliably bought the liveness it existed to protect.

Recorded deferral: limbo processes for vnodes this node never re-owns linger until the instance stops (full reconnect cleanup — killing on observing lost ownership — is an M7+ refinement). That is inside :continue's documented forfeit of guarantee 1 for side effects.

Summary

Functions

Returns a specification to start this module under a supervisor.

All limbo entries: [%{key: key_id, vnode: vnode_id, pid: pid}]. [] if no limbo.

The instance's limbo tracker name.

Kill every limbo entry belonging to vnode_id — called by a fresh impl incarnation at init/2, so stale fence survivors never double-serve a key on this node once it serves the vnode again. Raises (crashing that init) if the tracker is mid-restart: the agent's own restart retries the reap — skipping it silently could leave a survivor double-serving.

Start a limbo-supervised Fief.Key.Server. Same return shape as DynamicSupervisor.start_child/2, plus {:error, {:limbo_unavailable, reason}} in the (supervision-window) case where the tracker is mid-restart — the caller drops the triggering message, exactly like a refused start.

The instance's limbo DynamicSupervisor name.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

entries(instance)

@spec entries(atom()) :: [map()]

All limbo entries: [%{key: key_id, vnode: vnode_id, pid: pid}]. [] if no limbo.

name(instance)

@spec name(atom()) :: atom()

The instance's limbo tracker name.

reap(instance, vnode_id)

@spec reap(atom(), non_neg_integer()) :: :ok

Kill every limbo entry belonging to vnode_id — called by a fresh impl incarnation at init/2, so stale fence survivors never double-serve a key on this node once it serves the vnode again. Raises (crashing that init) if the tracker is mid-restart: the agent's own restart retries the reap — skipping it silently could leave a survivor double-serving.

start_key_server(instance, vnode_id, key_id, server_opts)

@spec start_key_server(atom(), non_neg_integer(), term(), keyword()) ::
  {:ok, pid()} | :ignore | {:error, term()}

Start a limbo-supervised Fief.Key.Server. Same return shape as DynamicSupervisor.start_child/2, plus {:error, {:limbo_unavailable, reason}} in the (supervision-window) case where the tracker is mid-restart — the caller drops the triggering message, exactly like a refused start.

start_link(opts)

sup_name(instance)

@spec sup_name(atom()) :: atom()

The instance's limbo DynamicSupervisor name.