Proper way to structure GenServer calls to self
Asked Answered
S

2

11

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).

Sprint answered 13/12, 2016 at 16:22 Comment(2)
I would make refill_queue a plain function and call it from handle_call(:pop) if you need it to be synchronous. Otherwise you have several options for handling it async (send another message to self, have another process handling refilling, etc)Tinworks
You should take a look at GenStage, sounds like it provides the functionality you are looking for.Kurtz
O
12

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.

Obcordate answered 13/12, 2016 at 18:39 Comment(3)
Yeah, I think this is pretty much exactly what I was wondering. I've wondered what the from argument was used for, and now it makes more sense.Sprint
I had no idea you could reply later, while you process other messages which is awesome to know. BUT! Won't this make it possible for multiple pop calls to call the refill at the same time? I mean, if refill takes 10ms and we have a pop call each 3ms we will get 2 pops (each triggering one more refill) while the first is doing the refill. Because that refill will go to the message queue, to be processed after the pops already in the message queue, right?Quimby
@IsmaelAbreu Yeah, you are right. You found a bug in my code. If you have a two pops in the message queue this will trigger two refills. The first refill will refill the stack. The second refill will not find a valid clause because the stack is not empty, so the server will crash with FunctionClauseError. I fixed the bug and added the new clause when the refills find a non-empty stack. Thank you :DObcordate
I
2

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.

Immortalize answered 13/12, 2016 at 17:26 Comment(1)
The basic distinction is that with my original implementation, I can refill the queue separately if I need to. There may be times when I want to do that independently of popping something off it. Of course, I can leave the refill_queue handle_call alone, which is kind of what I expect, but I'm just wondering if there's an idiomatic Elixir way of handling this kind of situation.Sprint

© 2022 - 2024 — McMap. All rights reserved.