MontyPlugin.Registry (ExMontyPlugin v0.1.0)

Copy Markdown View Source

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.

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

provider()

@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

child_spec(opts)

@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?()

@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(kind, name)

@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(kind, name, on)

@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(kind)

@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(kind, name, pid)

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

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

scope()

@spec scope() :: atom()

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

unregister(kind, name, pid)

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

Withdraws an offer.