# `MontyPlugin.Registry`
[🔗](https://github.com/d-led/ex.monty.plugin/blob/main/lib/monty_plugin/registry.ex#L1)

Which plugins a cluster offers, by name, on every node.

`:pg` rather than `Registry`: a `Registry` names processes on the node that
started it and knows nothing of the others, while `:pg` is OTP's cluster-wide
process group — a name joined on one node is visible on all of them, and a node
that goes down takes its names with it.

`:pg` rather than `Horde.Registry`, and the difference is what a name means.
Horde is a distributed `Registry` over a δ-CRDT, and it is the right tool for the
question it answers: one owner per name, handed to another node when a node
leaves. It supports `keys: :unique` only, and when two nodes register one name it
settles the conflict by sending the loser an exit signal. A plugin name is
deliberately *not* unique — every node of an estate may offer `follow_chamfers`,
and which of them can answer depends on where the part was opened — so the second
registration is a second provider, not a dead process. `:pg` holds exactly that:
many members per key, and a member is forgotten when its process or its node goes
away. A name that must have one owner (a run, a worker) is Horde's question, not
this one; the two sit in one cluster without argument.

## Two variables, both read at boot

12-factor says the cluster a node belongs to is a start-up decision, so this
module reads the environment and nothing else:

* `PLUGIN_SCOPE` — the `:pg` scope name (default `monty_plugins`). Two systems
  on one cluster that must not see each other's plugins differ here, exactly as
  they differ in node name.
* `PLUGIN_DISCOVERY` — `pg` (the default) or `none`. `none` is a deployment that
  runs its own built-in experiments and wants no cluster at all: registration
  answers `:ok` and does nothing, and a lookup says so rather than pretending
  the cluster is empty.

## What a provider is

A plugin is a process, and it answers `:describe`:

    def handle_call(:describe, _from, state), do: {:reply, MyPlugin.describe(), state}

The answer is held to `MontyPlugin.conforms?/1`. A plugin that describes itself
unusably is still *listed* — hiding it would make "nothing there" and "there but
unusable" look the same — and `describe` in its entry says which field is wrong.

# `provider`

```elixir
@type provider() :: %{
  kind: MontyPlugin.kind(),
  name: String.t(),
  node: node(),
  pid: pid(),
  describe: {:ok, map()} | {:error, String.t()}
}
```

One provider: where to call it, and what it says it is.

`describe` is `{:ok, map}` when the plugin answered with a description a cluster
can use, and `{:error, reason}` when it did not answer or answered unusably.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

The child spec that starts the `:pg` process under this scope.

The package starts it already (`MontyPlugin.Application`), so an application
does not add this to its own tree — it exists for a test that wants the process
under its own supervisor. `:pg` is an OTP module and has no `child_spec/1`, which
is why the start function is named here.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Whether this node takes part in a cluster at all, from `PLUGIN_DISCOVERY`.

Read at call time rather than cached, so the answer is the environment's answer
at every moment; the scope a query uses is fixed by the process started at boot.

# `lookup`

```elixir
@spec lookup(MontyPlugin.kind(), String.t()) ::
  {:ok, provider()} | {:error, String.t()}
```

The one provider under a name.

A name nobody offers is an error that names the ones there are, because the
caller is usually a model that can pick another.

This answers *a* provider, which is right for an `:anywhere` plugin — any node
may answer — and wrong for an `:owner_only` one, where the address decides
whether the call can be served at all. That question is `lookup/3`.

# `lookup`

```elixir
@spec lookup(MontyPlugin.kind(), String.t(), node()) ::
  {:ok, provider()} | {:error, String.t()}
```

The provider under a name, on a named node.

This is the question an `:owner_only` plugin asks: a motor policy that reads a
B-rep has to be called on the node that opened the part, and `lookup/2` answers
the first provider it finds — which may be the address of a copy that cannot
serve. The answer when it is not there names the nodes that do offer it, so a
caller can say which part is where instead of guessing.

# `providers`

```elixir
@spec providers(MontyPlugin.kind()) :: [provider()]
```

Every provider of a kind, across the cluster, in no particular order.

One plugin registered on two nodes appears twice, which is the truth: the caller
picks, and the node it picks is the node the work happens on.

# `register`

```elixir
@spec register(MontyPlugin.kind(), String.t(), pid()) :: :ok
```

Offers a process under a name, on this node and therefore on every node.

# `scope`

```elixir
@spec scope() :: atom()
```

The `:pg` scope this node registers in, from `PLUGIN_SCOPE`.

# `unregister`

```elixir
@spec unregister(MontyPlugin.kind(), String.t(), pid()) :: :ok
```

Withdraws an offer.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
