I'm trying to accomplish a simple task but I'm having huge difficulties.
Please suppose I have a GenServer
, and one of its callbacks is as follows:
@impl true
def handle_call(:state, _, state) do
# Something that would require 10 seconds
newState = do_job()
{:reply, newState, newState}
end
If I am right, invoking GenServer.call(:server, :state)
from a client side would block the server for 10 seconds, and then the new state would be returned to the client.
Okey. I want the server to handle this task without being blocked. I've tried using Tasks, but Task.await/2
and Task.yield/2
block the server.
I want the server not to block, and after those 10 seconds, receive the result on the client terminal. How would this be possible?
If I am right, invoking GenServer.call(:server, :state) from a client side would block the server for 10 seconds, and then the new state would be returned to the client.
Yes. Elixir does what you tell it to do, and in this line:
newState = do_job()
you are telling elixir to assign the return value of do_job()
to the variable newState
. The only way elixir can perform that assignment is by getting the return value of go_job()
....which will take 10 seconds.
I want the server not to block, and after those 10 seconds, receive the result on the client terminal.
One approach is for the GenServer to spawn()
a new process to execute the 10 second function and pass the pid of the client to the new process. When the new process gets the return value from the 10 second function, the new process can send()
a message to the client using the client pid.
That means the client would need to call handle_call()
rather than handle_cast()
because the server's implementation of handle_cast()
doesn't have a from
parameter variable containing the client pid. On the other hand, handle_call()
does receive the client pid in a from
parameter variable, so the server can pass the client pid to the spawned process. Note that spawn()
returns immediately, which means that handle_call()
can return immediately with a reply like :working_on_it
.
The next issue is: how will the client know when the new process that the GenServer spawned has finished executing the 10 second function? The client can't know when some extraneous process on the server has finished executing, so the client needs to wait in a receive until the message arrives from the spawned process. And, if the client is checking for messages in its mailbox, it would be helpful to know who the sender was, which means handle_call()
should also return the pid of the spawned process to the client. Another option for the client is to poll its mailbox every so often between bouts of doing other work. To do that, the client can define a receive with a short timeout in an after clause, then call a function in the after clause
to do some client work, followed by a recursive call to the function containing the receive so that the function checks the mailbox again.
Now what about Task
? According to the Task docs:
If you are using async tasks, you must await a reply...
Well, then what good is an async task if you must wait? Answer: if a process has at least two long running functions it needs to execute, then the process can use Task.async()
to run all the functions at the same time, rather than executing one function and waiting until it finishes, then executing another function and waiting until it finishes, then executing another one, etc.
But, Task also defines a start() function:
start(mod, fun, args)
Starts a task.
This is only used when the task is used for side-effects (i.e. no interest in the returned result) and it should not be linked to the current process.
That sounds like Task.start()
accomplishes what I described in the first approach. You need to define fun
so that it will run the 10 second function, then send a message back to the client after the 10 second function has finished executing (= the side-effect).
Below is a simple example of a GenServer that spawns a long running function, which allows the server to remain responsive to other client requests while the long running function executes:
a.exs:
defmodule Gen1.Server do
use GenServer
@impl true
def init(init_state) do
{:ok, init_state}
end
def long_func({pid, _ref}) do
Process.sleep 10_000
result = :dog
send(pid, {self(), result})
end
@impl true
def handle_call(:go_long, from, state) do
long_pid = spawn(Gen1.Server, :long_func, [from])
{:reply, long_pid, state}
end
def handle_call(:other, _from, state) do
{:reply, :other_stuff, state}
end
end
An iex session will be the client:
~/elixir_programs$ iex a.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, server_pid} = GenServer.start_link(Gen1.Server, [])
{:ok, #PID<0.93.0>}
iex(2)> long_pid = GenServer.call(server_pid, :go_long, 15_000)
#PID<0.100.0>
iex(3)> GenServer.call(server_pid, :other)
:other_stuff
iex(4)> receive do
...(4)> {^long_pid, reply} -> reply
...(4)> end
:dog
iex(7)>
A variable like long_pid
will match anything. To get long_pid
to match only its current value, you specify ^long_pid
(^
is called the pin operator).
A GenServer also allows you to block a client's call of handle_call()
while allowing the server to continue executing. That's useful if a client is unable to continue until it gets some needed data from the server, but you want the server to remain responsive to other clients. Here's an example of that:
defmodule Gen1.Server do
use GenServer
@impl true
def init(init_state) do
{:ok, init_state}
end
@impl true
def handle_call(:go_long, from, state) do
spawn(Gen1.Server, :long_func, [from])
{:noreply, state} #The server doesn't send anything to the client,
#so the client's call of handle_call() blocks until
#somebody calls GenServer.reply().
end
def long_func(from) do
Process.sleep 10_000
result = :dog
GenServer.reply(from, result)
end
end
In iex:
iex(1)> {:ok, server_pid} = GenServer.start_link(Gen1.Server, [])
{:ok, #PID<0.93.0>}
iex(2)> result = GenServer.call(server_pid, :go_long, 15_000)
...hangs for 10 seconds...
:dog
iex(3)>