mule/registry
Local process storage for Mule instances — a port of Oban.Registry.
Elixir’s registry is a Registry of unique via-tuple keys; gleam_otp has
no Registry/via-tuple equivalent and its static_supervisor exposes no
child lookup, so the honest substitute is a named public ETS table per
instance (FFI in mule_registry_ffi, the mule_cell_ffi crash-free
style) plus a small owner actor. The owner creates the table, monitors
every registrant, and deletes a registrant’s row on DOWN — deleting only
when the row still holds the dead pid, so a restarted process that
re-registered first is never wiped by the old registration’s DOWN.
lookup additionally filters dead pids, closing the gap between a crash
and the owner’s cleanup.
A persistent_term cell is deliberately NOT used here: registry rows are
created and deleted at runtime (queue start/stop, crashes), need
enumeration (check_all_queues), and need dead-entry cleanup — all things
persistent_term cannot do without global-GC cost and read-modify-write
races.
Handles are stored untyped in ETS; each role’s handle type is fixed by
its registrant (see Role), and typed accessors over the generic
lookup live in the modules that own the handle types (producer.subject,
mule.config) — the same discipline as the peer/Sonar cells.
Types
The row key, mirroring Oban.Registry’s Oban.name() | {Oban.name(), role()} keys: the instance itself is Instance; everything else is a
role. Each role’s handle type is fixed by its registrant:
Instance— handle: the assembledConfig, pid: root supervisorNotifier/Peer/Sonar/Stager/Foreman— handle:NilProducer(q)— handle:Subject(producer.Message)Queue(q)— handle:Nil, pid: the queue’s supervisorQueueForeman(q)— handle:foreman.Foreman(the task factory)Watchman(q)— handle:Subject(watchman.Message)Midwife— handle:Subject(midwife.Message)
pub type Role {
Instance
Foreman
Midwife
Notifier
Peer
Sonar
Stager
Queue(queue: String)
QueueForeman(queue: String)
Producer(queue: String)
Watchman(queue: String)
Owner
}
Constructors
-
Instance -
Foreman -
Midwife -
Notifier -
Peer -
Sonar -
Stager -
Queue(queue: String) -
QueueForeman(queue: String) -
Producer(queue: String) -
Watchman(queue: String) -
Owner
Values
pub fn child(
instance_name instance_name: String,
instance_handle instance_handle: handle,
) -> supervision.ChildSpecification(process.Subject(Message))
Child for the root supervisor, FIRST in the tree (so it survives notifier
crashes under RestForOne; a registry crash restarts everything after it,
which re-creates the table and re-registers every process). The start
closure runs in the root supervisor’s process, so process.self() there
IS the instance pid — captured and stored as the Instance row by the
owner’s initialiser, which also creates the table. instance_handle is
the assembled Config, held generically so this module imports nothing
from mule/config.
pub fn lookup(
instance_name instance_name: String,
role role: Role,
) -> Result(#(process.Pid, handle), Nil)
The generic lookup (Oban.Registry.lookup/2). Returns the row only when
the registered pid is alive, closing the gap between a crash and the
owner’s DOWN cleanup. The handle type is fixed per role by its registrant
(see Role); annotating a different type is a caller bug — the same
discipline as the peer/Sonar cells.
pub fn producers(
instance_name instance_name: String,
) -> List(#(String, process.Pid, handle))
Every live producer row for the instance, as #(queue, pid, handle) —
check_all_queues’s enumeration (mule.ex uses a Registry.select match
spec; a tab2list + filter is equivalent at these table sizes).
pub fn register(
instance_name instance_name: String,
role role: Role,
handle handle: handle,
) -> Nil
Register the CALLING process under role (producers call this in their
own initialisers, like via-tuple registration). Inserts the ETS row
directly, then asks the owner to monitor the caller for DOWN cleanup. A
total no-op when the instance’s table does not exist (a producer started
outside mule.start, as in tests).
pub fn register_child(
child child: supervision.ChildSpecification(data),
instance_name instance_name: String,
role role: Role,
) -> supervision.ChildSpecification(data)
Wrap a child spec so the started pid is registered under role with a
Nil handle — used on the notifier/peer/sonar/stager children for
whereis parity.
pub fn register_pid(
instance_name instance_name: String,
role role: Role,
pid pid: process.Pid,
handle handle: handle,
) -> Nil
Register a process OTHER than the caller — for wrapper closures registering children they just started.
pub fn unregister(
instance_name instance_name: String,
role role: Role,
) -> Nil
Delete a role’s row (the midwife’s stop_queue cleanup).
pub fn whereis(
instance_name instance_name: String,
role role: Role,
) -> option.Option(process.Pid)
The pid of a live registered process (Oban.Registry.whereis/2).