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)
.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With