elixir

How do I use a registry within a supervision tree with child_spec name: {:via} syntax for other supervisor children?


I am learning elixir & I am struggling a bit with how to set up separate processes for data access & API routing while allowing them to communicate in a way that allows either process to fail while still being able to reconnect / be connected to via the new child process upon the next request. It seems that the Registry module is the answer for where to store this information, but I am struggling to get it to work the way I expect just from reading the hexdocs & am finding the {:via, ...} syntax is not setting my registry key.

Currently, I have a supervisor which has 3 child processes - a Registry, a GenServer (storage) & a Plug.Router module. I initialize the registry first, then the storage using name: {:via, ...} to try & store the storage module's PID in the registry. Last I want to access that storage key in the registry in the router, but I haven't gotten that far yet as I am struggling to get the registry key to register.

I was able to get the router to have the GenServer PID by passing it explicitly as an argument on application start & making the router store it as an Agent so I know my router / GenServer code works when I do that, but this approach is very weak & means if the GenServer fails, the router has an out of date PID (defeating the whole purpose of having the supervisor for restarting processes).

My start code in the application module:

def start(_, _) do 
    opts = [strategy: :one_for_one, name: Usersystem.Supervisor]

    {:ok, supervisor} = Supervisor.start_link([], opts)

    {:ok, _} = Supervisor.start_child(supervisor, {Registry, keys: :unique, name: Usersystem.Registry})

    {:ok, _storage} = Supervisor.start_child(supervisor, {Usersystem.Storage, name: {:via, Registry, {Usersystem.Registry, "storage"}}})

    {:ok, _router} = Supervisor.start_child(supervisor, {Plug.Cowboy, scheme: :http, plug: Usersystem.Router, options: [port: 8080] })

    res = Registry.lookup(Usersystem.Registry, "storage")
    Logger.info(res) # I get an empty result instead of [{pid(), value}], which tells me the storage key is not being assigned to. 

    {:ok, supervisor}
end

Can anyone give me some advice about what I am missing in this start function that is preventing this key from being initialized using the {:via, ...} syntax, or if I am misunderstanding any part of how registry's work with this code? Also open to the idea that I'm barking up the wrong tree, as I am quite new to Elixir. From what I understand I could use named processes as well, but I would think using Registry gives me a bit more control about what I do w/ my processes down the line so I would like to learn how it works.

I am using Elixir 17.3 & Erlang/OTP 27.


Solution

  • To directly answer your question, the problem is in the {:ok, _storage} = Supervisor.start_child(supervisor, {Usersystem.Storage, name: {:via, Registry, {Usersystem.Registry, "storage"}}}). First, there is no name in child_spec, the closest field you might find is the id, but it also does not solve your issue, since this id is local to the supervisor. Your tuple is valid enough for starting the supervisor, but the {:via, Registry, _} does not mean registering it in the registry table. You need to add this tuple to the Usersystem.Storage server instead:

    defmodule Stackoverflow do
      require Logger
    
      def start() do
        opts = [strategy: :one_for_one, name: Usersystem.Supervisor]
    
        {:ok, supervisor} = Supervisor.start_link([], opts)
    
        # The supervisor starts a registry table, which can be used by servers later
        {:ok, _} =
          Supervisor.start_child(supervisor, {Registry, keys: :unique, name: Usersystem.Registry})
    
        {:ok, _storage} =
          Supervisor.start_child(
            supervisor,
            # A map child spec, check https://hexdocs.pm/elixir/1.12/Supervisor.html#child_spec/2
            %{
              # {Module callback, function callback from module, params to the function (in this case ignored)}
              start: {Usersystem.Storage, :start_link, [:ok]},
              # This id is internal to the supervisor, it is only recognizable under `Usersystem.Supervisor`
              id: :storage
            }
          )
    
        # I did not imported Plug.Cowboy since this example does not need it
        # {:ok, _router} = Supervisor.start_child(supervisor, {Plug.Cowboy, scheme: :http, plug: Usersystem.Router, options: [port: 8080] })
    
        res = Registry.lookup(Usersystem.Registry, "storage")
    
        sup_children = Supervisor.which_children(Usersystem.Supervisor)
    
        Logger.info("registry response: #{inspect(res)}")
        # [info] registry response: [{#PID<0.149.0>, nil}]
    
        # I will not log the long response, but note how the supervisor logs the storage child with the `:storage` id we provided
        Logger.info("Supervisors children response: #{inspect(sup_children)}")
    
        {:ok, supervisor}
      end
    end
    
    defmodule Usersystem.Storage do
      use GenServer
    
      def start_link(_) do
        # This will register the server properly in the Usersystem.Registry table under "storage"
        GenServer.start_link(__MODULE__, [], name: {:via, Registry, {Usersystem.Registry, "storage"}})
      end
    
      def init(_), do: {:ok, nil}
    end
    

    However, if you have only one storage server, maybe you don't even need the Registry. instead of GenServer.start_link(__MODULE__, [], name: {:via, Registry, {Usersystem.Registry, "storage"}}), you could just do GenServer.start_link(__MODULE__, [], name: :my_storage_server. Which makes the server start under the atom name you provided. Note that you could name this as :storage and it would not conflict with the supervisor child id also called :storage at all, since the sup id is internal, I'm just using a different name to make this example clearer. You can verify the name is reachable, by simply starting the supervisor and typing: Process.where_is(:my_storage_server), which will return the id of your server. When your server restarts, it will be registered under the same atom name, so it will be available without knowing its pid. Since it is a genserver, any process calling the GenServer.call/cast passing the my_storage_server as first parameter will find the storage server.

    Some notes: