The use for keys: :duplicate in Registries?

I find it surprising that :via is not supported for duplicate registries. If you can’t register a pool of processes under the same name what exactly is the use-case for having duplicate-key registries? The docs include an example of dispatching (Registry — Elixir v1.18.0-dev), but I’m not clear how you would even put a process into a custom Registry if :via is not allowed. What am I missing? How do you actually use a custom Registry with duplicate keys?

Registry.register exists. You can manually (and explicitly) call it. :via is just a way to let OTP take over the process registration. But that comes with the downside that via expects a given via tuple to map to a single process, not multiple ones.

Process pooling is not included in the standard library of Elixir. There a few excellent libraries on hex.pm:

Some use-cases for having duplicate-key registries are explained in your linked documentation, if only you read a few more paragraphs from that point, you will find them.

:via isn’t specifically related to Registry, it’s just a fancy way to tell :gen’s name handling to call register_name and whereis_name on a specified module:

(related: Elixir’s wrappers transform a bare atom name into {:local, name})

Conversely, Registry isn’t otherwise related to process naming - it’s just a key-value store that automatically cleans up keys when the registering process exits.

The simplest thing to build with a :duplicate registry is a local publish-subscribe bus:

  • processes that are interested in a topic {:foo, 123} use Registry.register to “subscribe” to that key
  • when a message needs to be delivered for topic {:foo, 123}, the sender uses Registry.dispatch to send it to each PID
  • when a process that’s subscribed to a topic stops, its registration is automatically cleaned up

You might even use both a :unique and a :duplicate registry with the same keys, for instance in the classic “multiplayer game” architecture:

  • the :unique registry tracks a GenServer per “game”. A :via tuple pointing to this registry is used when sending moves, joins, etc to the game
  • the :duplicate registry tracks the Liveview processes of players per “game”, and is used to distribute updates to all players
3 Likes

Thank you for the informative responses! This is stuff that I’m going to have to ponder about for a while, but I see the set/get is pretty straightforward using Registry.register/3 and Registry.lookup/2:

iex> {:ok, _} = Registry.start_link(keys: :duplicate, name: ClusterRegistry)
iex> Registry.register(ClusterRegistry, :group, "a")
iex> Registry.register(ClusterRegistry, :group, "b")
iex> Registry.register(ClusterRegistry, :group, "c")
iex> Registry.lookup(ClusterRegistry, :group)
[{#PID<0.148.0>, "a"}, {#PID<0.148.0>, "b"}, {#PID<0.148.0>, "c"}]

What is interesting to me is that Registry.register/3 really is all about storing the current process – the value seems almost secondary. The other thing that is interesting is that when a process terminates, anything that it registered gets removed (as. you pointed out Matt):

iex> Task.start(fn -> 
  Registry.register(ClusterRegistry, :group, "hello?") 
  Process.sleep(10_000)
end)
iex> Registry.lookup(ClusterRegistry, :group)
[{#PID<0.148.0>, "a"}, {#PID<0.148.0>, "b"}, {#PID<0.148.0>, "c"}, {#PID<0.148.0>, "hello?"}]

# Wait 10 seconds
iex> Registry.lookup(ClusterRegistry, :group)
[{#PID<0.148.0>, "a"}, {#PID<0.148.0>, "b"}, {#PID<0.148.0>, "c"}]

I feel like I’m only becoming dimly aware of the things that you can do with these tools…

1 Like

That’s true indeed. In the end it’s a registry, so exists to hold registrations of processes. But the value can be very useful for certain usecases within the space of process registration.