Lucas van Dongen
Lucas van Dongen

Reputation: 9878

Using a GenServer in Phoenix - the process is not alive

I'm trying to get my head around working with a GenServer in Phoenix to accumulate information received from different clients to a web socket. The GenServer looks like this:

 defmodule HelloWeb.Stack do
    use GenServer

    @name {:global, __MODULE__} # this seems to help me to prevent having to drag the pid everywhere in the application

    # Client
    def start_link do
      #GenServer.start_link(__MODULE__, [])
      GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end

    def add(item) do
      GenServer.cast(@name, item)
    end

    def view do
      GenServer.call(@name, :view)
    end

    def remove(item) do
      GenServer.cast(@name, {:remove, item})
    end

    def wipe do
      GenServer.cast(@name, :wipe)
    end

    #Server
    def init(list) do
      {:ok, list}
    end

    def handle_cast(:wipe, _list) do
      updated_list = []
      {:noreply, updated_list}
    end

    def handle_cast({:remove, item}, list) do
      updated_list = Enum.reject(list, fn(i) -> i == item end)
      {:noreply, updated_list}
    end

    def handle_cast(item, list) do
      updated_list = [item|list]
      {:noreply, updated_list}
    end

    def handle_call(:view, _from, list) do
      {:reply, list, list}
    end
  end

This is the relevant part of my Phoenix channel:

def handle_in("answer", payload, socket) do
  HelloWeb.Stack.add(payload)
  {:noreply, socket}
end

def handle_in("answers", _payload, socket) do
  {:reply, {:ok, HelloWeb.Stack.view}, socket}
end

The weird thing is that "answer" seems to work every time I call it but "answers" always crashes the GenServer:

[error] GenServer #PID<0.10032.0> terminating ** (stop) exited in: GenServer.call({:global, HelloWeb.Stack}, :view, 5000) ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

I'm not sure how to share one GenServer between multiple channel actions. If I use iex all of the functions work exactly as I expect them to work, but in Phoenix the initialization and usage are spread.

I try to start the GenServer in Application as follows:

defmodule Hello.Application do
  use Application

  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec

    # Define workers and child supervisors to be supervised
    children = [
      # Start the endpoint when the application starts
      supervisor(HelloWeb.Endpoint, []),
      # Start your own worker by calling: Hello.Worker.start_link(arg1, arg2, arg3)
      worker(HelloWeb.Stack, []),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Hello.Supervisor]
    Supervisor.start_link(children, opts)
  end

Sadly I have trouble debugging into the system and I can't figure out yet how to do that right. Lots of new things to absorb at once and I'm still a few a-ha's away from Elixir enlightenment. I have added two debug endpoints to debug things in very crude way:

def handle_in("debug2", _payload, socket) do
  {:reply, {:ok, %{ genserver: GenServer.whereis(HelloWeb.Stack) }}, socket}
end

def handle_in("debug1", _payload, socket) do
  {:ok, pid} = HelloWeb.Stack.start_link
  {:reply, {:ok, %{ genserver: GenServer.whereis(HelloWeb.Stack), pid: :erlang.pid_to_list(pid) }}, socket}
end

debug2 just keeps returning null for HelloWeb.Stack, but when I run debug1 the first time I get null and a valid pid, the second time it crashes with:

** (MatchError) no match of right hand side value: {:error, {:already_started, #PID<0.12310.0>}}

This seems to indicate that it starts successfully, binds itself to a unique value and doing that for a second time is rejected because that particular process is already started. However I cannot use HelloWeb.Stack to reach it. Let me show the other relevant parts of the code after doing some changes and tests:

defmodule HelloWeb.Stack do
    use GenServer

    @name {:global,  __MODULE__}

    # Client
    def start_link do
      #GenServer.start_link(__MODULE__, [])
      GenServer.start_link(__MODULE__, [], name: @name)
    end

This is how the worker is started in application.ex:

worker(HelloWeb.Stack, []), #how to verify it's really starting something and to what value it stores it's pid?

Upvotes: 4

Views: 5608

Answers (2)

sebisnow
sebisnow

Reputation: 2077

HelloWeb.Stack.start_link/1 is not defined and thus cannot be called

You implement the function start_link/0. It does not take any arguments. In the docs I usually see start_link/1 being implemented in order to start your GenServer.

As I read the GenServer docs, HelloWeb.Stack.start_link/1 is always invoked when you start your GenServer as part of the supervision tree. In your case HelloWeb.Stack.start_link/1 is not defined and thus cannot be called.

So I suggest you implement start_link/1 like:

# Client
def start_link(_init_args) do
  #GenServer.start_link(__MODULE__, [])
  GenServer.start_link(__MODULE__, [], name: HelloWeb.Stack)
end

What happens internally is something the Elixir/Erlang experts have to answer (Edits are very welcome!), but I think this is the root of your problem.

Upvotes: 2

Harrison Lucas
Harrison Lucas

Reputation: 2961

Sounds like you aren't starting the GenServer with the application and therefore, the message is correct - e.g. there is no process alive.

The reason the HelloWeb.Stack.add(payload) function still works is because that's the intention of the cast functionality of GenServer's, e.g. you cast to the process because you don't care about the returned result at and whether it succeeds or fails (otherwise you would use call)

For example:

iex(1)> GenServer.cast(:non_existing_genserver, :add)
:ok

Here you can see that even though the GenServer doesn't exist, cast still returns an ack message, but in reality it's not going to go anywhere.

To solve your problem, navigate to your application.ex file and add in a worker to start the GenServer when the application starts up.

children = [
  #...other workers/supervisors
  {HelloWeb, []}
]

Upvotes: 2

Related Questions