Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to structure GenServer calls to self

I know it's pretty much impossible to have a GenServer process call itself because you essentially hit a deadlock. But, I'm curious if there's a preferred way to do this kind of thing.

Assume the following scenario: I've got a queue that I'm popping things from. If the queue is ever empty, I want to refill it. I could structure it like so:

def handle_call(:refill_queue, state) do
  new_state = put_some_stuff_in_queue(state)
  {:reply, new_state}
end

def handle_call(:pop, state) do
  if is_empty_queue(state) do
    GenServer.call(self, :refill_queue)
  end

  val,new_state = pop_something(state)

  {:reply, val, new_state}
end

The big problem here is that this will deadlock when we try to refill the queue. One solution that I've used in the past is to use cast more so it doesn't deadlock. Like so (change call to cast for refill)

def handle_cast(:refill_queue, state) do

But in this case, I think it won't work, since the async cast to refill the queue might return in the pop case before actually filling the queue meaning I'll try to pop off an empty queue.

Anyways, the core question is: What is the best way to handle this? I assume the answer is to just call put_some_stuff_in_queue directly inside the pop call, but I wanted to check. In other words, it seems like the right thing to do is make handle_call and handle_cast as simple as possible and basically just wrappers to other functions where the real work happens. Then, create as many handle_* functions as you need to cover all the possible cases you'll deal with, rather than having handle_call(:foo) in turn call handle_call(:bar).

like image 971
Micah Avatar asked Dec 13 '16 16:12

Micah


2 Answers

There's a function in GenServer module called reply/2. The second argument of the handle_call/3 callback is the connection to the client. You can create a new process to handle the connection and return {:noreply, state} in the callback clause. Using your example:

defmodule Q do
  use GenServer

  ############
  # Public API

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

  def push(pid, x) do
    GenServer.call(pid, {:push, x})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  ########
  # Helper

  # Creates a new process and does a request to
  # itself with the message `:refill`. Replies
  # to the client using `from`.
  defp refill(from) do
    pid = self()
    spawn_link fn ->
      result = GenServer.call(pid, :refill)
      GenServer.reply(from, result)
    end
  end

  ##########
  # Callback

  def handle_call(:refill, _from, []) do
    {:reply, 1, [2, 3]}
  end
  def handle_call(:refill, _from, [x | xs]) do
     {:reply, x, xs}
  end
  def handle_call({:push, x}, _from, xs) when is_list(xs) do
    {:reply, :ok, [x | xs]}
  end
  def handle_call(:pop, from, []) do
    # Handles refill and the reply to from.
    refill(from)
    # Returns nothing to the client, but unblocks the
    # server to get more requests.
    {:noreply, []}
  end
  def handle_call(:pop, _from, [x | xs]) do
    {:reply, x, xs}
  end
end

And you would get the following:

iex(1)> {:ok, pid} = Q.start_link()
{:ok, #PID<0.193.0>}
iex(2)> Q.pop(pid)
1
iex(3)> Q.pop(pid)
2
iex(4)> Q.pop(pid)
3
iex(5)> Q.pop(pid)
1
iex(6)> Q.pop(pid)
2
iex(7)> Q.pop(pid)
3
iex(8)> Q.push(pid, 4)
:ok
iex(9)> Q.pop(pid)    
4
iex(10)> Q.pop(pid)
1
iex(11)> Q.pop(pid)
2
iex(12)> Q.pop(pid)
3
iex(13)> tasks = for i <- 1..10 do
...(13)>   Task.async(fn -> {"Process #{inspect i}", Q.pop(pid)} end)
...(13)> end
(...)
iex(14)> for task <- tasks, do: Task.await(task)
[{"Process 1", 1}, {"Process 2", 2}, {"Process 3", 1}, {"Process 4", 2},
 {"Process 5", 3}, {"Process 6", 3}, {"Process 7", 2}, {"Process 8", 1},
 {"Process 9", 1}, {"Process 10", 3}]

So it is in fact possible for a GenServer to do requests to itself. You just need to know how.

I hope this helps.

like image 93
Alex de Sousa Avatar answered Nov 16 '22 22:11

Alex de Sousa


why do you need to make GenServer.call?

def handle_call(:pop, state) do
  new_state0 = if is_empty_queue(state) do
     put_some_stuff_in_queue(state)
  else
     state
  end
  {val,new_state} = pop_something(new_state0)

  {:reply, val, new_state}
end

or

def handle_call(:pop, state) do
  {val, new_state} = state
                     |> is_empty_queue
                     |> case do
                          true ->
                            put_some_stuff_in_queue(state)
                          false ->
                            state
                        end
                     |> pop_something

  {:reply, val, new_state}
end

so making calls is no-no but calling other functions is totally doable.

like image 29
ash Avatar answered Nov 16 '22 20:11

ash