<!-- Generated by docs/tools/registry_flows.py. Edit the script, then run it. -->

# dgen_registry Message Flows

Each diagram follows one operation through the processes that take part. Time runs
downward. Message names are the tuples in the code, and `E` and `V` are the epoch
and the commit version. A member's loop never blocks on another member. Calls
between members run in spawned helpers or come from the elector.

Section numbers like §5.6 refer to the [dgen_registry Design](dgen_registry_design.md).

![Arrow styles: cast, call, reply, durable, monitor signal, the highlighted hop, and a lost message](assets/registry_flows/legend.svg)

**This document is LLM generated.**

## Register through a follower

A follower forwards the registration to the leader, which commits it, broadcasts the batch, then replies. FIFO puts the batch ahead of the reply, so the follower already holds the binding when it answers yes.

![Swimlane diagram: Register through a follower](assets/registry_flows/forwarded.svg)

- The follower's loop never blocks on the leader. Every hop between members is a cast tagged with `Ref`.
- The leader answers forwarded registrations as soon as they commit. The forwarding follower is the second holder, so no replica ack is needed.
- The worker only bumps the version key. Name state lives in members' memory. The leader key read is what fences a deposed leader.

In the code: `dgen_registry_member`: register call, register_req, start_commit/1, apply_committed_plan/3, names_batch handler, register_reply handler.

## Register on the leader's node

A registration made on the leader's own node commits, then waits for register_replicas followers to confirm they hold the batch before the caller gets yes.

![Swimlane diagram: Register on the leader’s node](assets/registry_flows/direct.svg)

- `replicate_sync` is sent after the batch on the same channel, so FIFO delivers it only once the follower has seen the batch.
- A follower that refused the batch as a gap stays silent. It never claims to hold bindings it doesn't have.
- With no followers at all, the leader answers `yes` right away.

In the code: `dgen_registry_member`: await_replica_acks/3, replicate_sync handler, replicate_ack handler, replicate_timeout handler.

## Unregister and process exit

An unregister deletes the local row at once and is committed by the leader. When a registered process exits, the leader's tagged monitor turns the exit into the same unregister batch.

![Swimlane diagram: Unregister and process exit](assets/registry_flows/unregister.svg)

- Only the leader monitors registered pids. The monitor's custom tag carries the name, so the DOWN says which name to clear.
- A retract only clears the name if the same pid still holds it, so it never removes a later registration.

In the code: `dgen_registry_member`: route_unregister/3, unregister_req, {down, Name} handler, monitor_name/2.

## Snapshot and consistent reads

A snapshot read is a lock-free ETS lookup with no messages. A consistent read goes to the leader, and a helper checks the durable leader key before it replies to the caller directly.

![Swimlane diagram: Snapshot and consistent reads](assets/registry_flows/reads.svg)

- The snapshot table is `protected`. The member is its only writer, and callers read it without locking.
- `get_metadata_consistent/1` and `query_consistent/2` take the same path with their own request tags.
- Neither the follower nor the leader waits for the backend. The reply goes straight to the caller's `From`.

In the code: `dgen_registry`: whereis_name/1; `dgen_registry_member`: forward_read/5, whereis_req, reply_fenced/4.

## A member joins

A joining member announces itself through the elector's durable queue. The elector keeps the sticky leader, and the leader onboards the joiner with one snapshot while existing followers only start monitoring it.

![Swimlane diagram: A member joins](assets/registry_flows/join.svg)

- Every node runs an elector process over the same durable state. Whichever one consumes the `join` runs the action.
- Because the snapshot comes from the leader process, FIFO puts it ahead of every later `names_batch` the joiner sees.
- The first member to join finds no leader, so the elector elects it with a new epoch and it takes the full handoff path.

In the code: `dgen_registry_elector`: join transaction; `dgen_registry_elector_actions`: push_leader_to_members/1; `dgen_registry_member`: leader assume call, assume_leadership_fast_path/6, cast_data_to_new_member/4.

## Leader failure and handoff

When the leader dies, survivors report it to the elector, which elects a new leader in a new epoch. The new leader gathers every replica, waits until it has caught up to the durable version, and sends each member a snapshot in the new epoch.

![Swimlane diagram: Leader failure and handoff](assets/registry_flows/failover.svg)

- F sends its own `member_down` too. The elector drops the second one because L0 is already gone.
- The new leader key fences L0. If L0 is alive but cut off, its next commit aborts.
- Ending at the deadline without reaching the frontier marks the leader `degraded`. A committed binding may be missing, and §5.6 repairs it.
- A forwarded registration that reaches N while it is assuming is postponed until the gather finishes.

In the code: `dgen_registry_member`: peer DOWN handler, prepare_to_assume_leadership/6, gather_until_caught_up/7, assume_gathered; `dgen_registry_elector`: member_down transaction, elect_leader/5.

## Gap detection and resync

Each batch names the version it follows. A follower that sees a gap applies nothing and asks the leader for a snapshot. The heartbeat exposes a lost tail, and a batch from a later epoch always counts as a gap.

![Swimlane diagram: Gap detection and resync](assets/registry_flows/resync.svg)

- Applying batches whole and in order keeps every replica a prefix of the leader's stream. Two replicas at the same version hold the same content.
- A batch from an older epoch is dropped as stale, and one at or behind the applied version is a duplicate.
- The leader sends the heartbeat every 5 s.

In the code: `dgen_registry_replica`: offer_batch/5; `dgen_registry_member`: request_resync/2, resync_req, broadcast_heartbeat/1.

## A held yes across a leader change

A follower holding a yes from epoch E drops it when a snapshot moves it into a later epoch, and sends the registration to the new leader again instead of answering from versions the new leader produced.

![Swimlane diagram: A held yes across a leader change](assets/registry_flows/held-yes.svg)

- A yes from an epoch F has already left is re-dispatched along the current route, not held.
- A yes from an epoch F hasn't reached yet is held, and F asks for a resync to get there.
- The TLA+ config `NoEpochFence` turns this off and finds the false ack.

In the code: `dgen_registry_intents`: on_reply/5, on_version/2; `dgen_registry_replica`: offer_batch/5; `dgen_registry_member`: apply_names_snapshot.

## A deposed leader is fenced

A leader cut off from the cluster keeps trying to commit. The backend rejects its commit because the leader key names a newer leader, and the old leader hands its work back and rejoins.

![Swimlane diagram: A deposed leader is fenced](assets/registry_flows/fenced.svg)

- The commit reads the leader key, or adds a read conflict on it. A newer leader key aborts the commit.
- A forwarded registration in the failed batch is dropped. Its follower re-drives it to the new leader.

In the code: `dgen_registry_member`: handle_commit_result/3, maybe_rejoin/1; `dgen_registry_names`: verify_leader/4.
