Search code examples
elixirgen-server

Calling a GenServer from another GenServer


I have a project where I use 2 GenServers The first GenServer named "State" maintains the state, the second GenServer named "Updates" maintains a list of possible updates to the state. What I want to achieve is:

With every call to "State", "State" should call "Updates" and update itself before returning the actual state.

Both GenServers are started by a supervisor and I can call both GenServers by name from outside but if I try to call the API of "Updates" inside of "State", "State" terminates with a "no process" error. Any suggestions?

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(_arg) do
    children = [
      {Updates, name: Updates},
      {State, name: State}
    ]

    Supervisor.init(children, strategy: :rest_for_one)
  end

Both GenServer start with

  def start_link(opts) do
    GenServer.start_link(__MODULE__, [], opts)
  end

In "State" I have the callback

  @impl true
  def handle_call({:get}, _from, state) do
    updates = Updates.get_updates(Updates)
    {:reply, updates, state}
  end

Again, if I call Updates.get_updates(Updates) for example from iex directly everything works as expected, so I think everything is fine with my supervisor. It seems just like "State" does not know the name of "Updates".

Updates.get_updates/1 implementation is:

  def get_updates(pid) do
    GenServer.call(pid, :get)
  end

where the callback is just the state as reply

  @impl true
  def handle_call(:get, _from, state) do
    {:reply, state, state}
  end

Solution

  • State" terminates with a "no process" error. Any suggestions?

    According to the Supervisor docs, the children list:

       children = [
          {Updates, name: Updates},
          {State, name: State}
        ]
    

    should be a list of child specification tuples, where a child specification has the following valid keys:

    The child specification contains 6 keys. The first two are required, and the remaining ones are optional:

    :id - any term used to identify the child specification internally by the supervisor; defaults to the given module. In the case of conflicting :id values, the supervisor will refuse to initialize and require explicit IDs. This key is required.

    :start - a tuple with the module-function-args to be invoked to start the child process. This key is required.

    :restart - an atom that defines when a terminated child process should be restarted (see the “Restart values” section below). This key is optional and defaults to :permanent.

    :shutdown - an atom that defines how a child process should be terminated (see the “Shutdown values” section below). This key is optional and defaults to 5000 if the type is :worker or :infinity if the type is :supervisor.

    :type - specifies that the child process is a :worker or a :supervisor. This key is optional and defaults to :worker.

    There is a sixth key, :modules, that is rarely changed. It is set automatically based on the value in :start.

    Note that there is no name: key, which you are listing in your child specifications.

    However, GenServer.start_link() does have a name: option:

    Both start_link/3 and start/3 support the GenServer to register a name on start via the :name option. Registered names are also automatically cleaned up on termination. The supported values are:

    an atom - the GenServer is registered locally with the given name using Process.register/2.

    {:global, term} - the GenServer is registered globally with the given term using the functions in the :global module.

    {:via, module, term} - the GenServer is registered with the given mechanism and name. The :via option expects a module that exports register_name/2, unregister_name/1, whereis_name/1 and send/2. One such example is the :global module which uses these functions for keeping the list of names of processes and their associated PIDs that are available globally for a network of Elixir nodes. Elixir also ships with a local, decentralized and scalable registry called Registry for locally storing names that are generated dynamically.

    For example, we could start and register our Stack server locally as follows:

    # Start the server and register it locally with name:
    
    MyStack {:ok, _} = GenServer.start_link(Stack, [:hello], name: MyStack)
    

    So, I think you should be doing something like this:

      def init(_arg) do
        children = [
          Updates,
          State
        ]
    

    Then in your GenServer start_link() functions:

     def start_link(args) do
        GenServer.start_link(__MODULE__, args, name: __MODULE__)
     end
    

    ======

    Here is a full example. In application.ex, you could specify the names that you want to register:

    children = [
      # Starts a worker by calling: Servers.Worker.start_link(arg)
      # {Servers.Worker, arg},
      {
        Servers.CurrentState, [ 
          init_state_with: [:hello, 10], 
          name_to_register: Servers.CurrentState
        ] 
      },
      {
        Servers.Updates, [
          init_state_with: [:goodbye], 
          name_to_register: Servers.Updates
        ]
      }
    ]
    

    Then you could define your two GenServers like this:

    lib/servers/updates.ex:

    defmodule Servers.Updates do
      use GenServer
    
      def start_link(arg) do  
    
        GenServer.start_link(
          __MODULE__, 
          arg[:init_state_with], 
          name: arg[:name_to_register])                                       
    
      end
    
      ## Callbacks
    
      @impl true
      def init(state) do
        {:ok, state}
      end
    
      @impl true
      def handle_call(:get_updates, _from, state) do
        {:reply, state, state}
      end
    
      @impl true
      def handle_cast({:push, item}, state) do
        {:noreply, [item | state]}
      end
    
    
      ##User interface:
    
      def get() do
        GenServer.call(__MODULE__, :get_updates)
      end
    
      def add(item) do
        GenServer.cast(__MODULE__, {:push, item})
      end
    
    end
    

    lib/servers/current_state.ex:

    defmodule Servers.CurrentState do
      use GenServer
    
      def start_link(args) do  
    
        GenServer.start_link(
          __MODULE__, 
          args[:init_state_with], 
          name: args[:name_to_register])
    
      end
    
      ## Callbacks
    
      @impl true
      def init(state) do
        IO.inspect(state, label: "The CurrentState server is starting with state")
        {:ok, state}
      end
    
      @impl true
      def handle_call(:get_state, _from, state) do
        state_to_add = Servers.Updates.get()
        new_state = state_to_add ++ state
    
        {:reply, new_state, new_state}
      end
    
    
      ##User interface:
    
      def get() do
        GenServer.call(__MODULE__, :get_state)
      end
    
    end
    

    Then, you can test things with:

    defmodule Servers.Go do
      def test() do
        IO.inspect("Updates has state: #{inspect Servers.Updates.get()}" )
        IO.inspect("CurrentState has state: #{inspect Servers.CurrentState.get()}" )
        :ok
      end
    end
    

    In iex:

    ~/elixir_programs/servers$ iex -S mix
    Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
    
    Compiling 1 file (.ex)
    
    The CurrentState server is starting with state: [:hello, 10]
    
    Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)
    
    iex(1)> Servers.Go.test()
    "Updates has state: [:goodbye]"
    "CurrentState has state: [:goodbye, :hello, 10]"
    :ok
    
    iex(2)> 
    

    (Note the first line of output is mixed in with the server startup messages.)

    However, you can use __MODULE__ to simplify things:

    application.ex:

    children = [
      # Starts a worker by calling: Servers.Worker.start_link(arg)
      # {Servers.Worker, arg},
    
      { Servers.CurrentState,  [:hello, 10] }
      { Servers.Updates, [:goodbye] }
    
    ]
    

    lib/servers/updates.ex:

    defmodule Servers.Updates do
      use GenServer
    
      def start_link(arg) do  
                      #arg comes from child specification tuple
                      #inside the `children` list in application.ex
    
        #                        | module where the GenServer is defined
        #                        | 
        #                        |        | send arg to the GenServer's init() function       
        #                        V        V
        GenServer.start_link(__MODULE__, arg, name: __MODULE__)
        #                                      ^
        #                                      |
        #                     register the specified name for this GenServer
    
      end
    
      ## Callbacks
    
      @impl true
      def init(state) do
        {:ok, state}
      end
    
      @impl true
      def handle_call(:get_updates, _from, state) do
        {:reply, state, state}
      end
    
      @impl true
      def handle_cast({:push, item}, state) do
        {:noreply, [item | state]}
      end
    
      ## User interface:
    
      def get() do
        GenServer.call(__MODULE__, :get_updates)
      end
    
      def add(item) do
        GenServer.cast(__MODULE__, {:push, item})
      end
    
    end
    

    lib/servers/current_state.ex:

    defmodule Servers.CurrentState do
      use GenServer
    
      def start_link(arg) do  
                      #arg comes from child specification tuple
                      #inside the `children` list in application.ex
    
        #                        | module where the GenServer is defined
        #                        | 
        #                        |        | send arg to the GenServer's init() function       
        #                        V        V
        GenServer.start_link(__MODULE__, arg, name: __MODULE__)
        #                                       ^
        #                                       |
        #                     register the specified name for this GenServer 
      end
    
      ## Callbacks
    
      @impl true
      def init(state) do
        IO.inspect(state, label: "The CurrentState server is starting with state")
        {:ok, state}
      end
    
      @impl true
      def handle_call(:get_state, _from, state) do
        state_to_add = Servers.Updates.get()
        new_state = state_to_add ++ state
    
        {:reply, new_state, new_state}
      end
    
      ## User interface:
    
      def get() do
        GenServer.call(__MODULE__, :get_state)
      end
    
    end