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:pgscope name (defaultmonty_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) ornone.noneis a deployment that runs its own built-in experiments and wants no cluster at all: registration answers:okand 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.
Summary
Types
One provider: where to call it, and what it says it is.
Functions
The child spec that starts the :pg process under this scope.
Whether this node takes part in a cluster at all, from PLUGIN_DISCOVERY.
The one provider under a name.
The provider under a name, on a named node.
Every provider of a kind, across the cluster, in no particular order.
Offers a process under a name, on this node and therefore on every node.
The :pg scope this node registers in, from PLUGIN_SCOPE.
Withdraws an offer.
Types
@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.
Functions
@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.
@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.
@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.
@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.
@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.
@spec register(MontyPlugin.kind(), String.t(), pid()) :: :ok
Offers a process under a name, on this node and therefore on every node.
@spec scope() :: atom()
The :pg scope this node registers in, from PLUGIN_SCOPE.
@spec unregister(MontyPlugin.kind(), String.t(), pid()) :: :ok
Withdraws an offer.