Reputation: 1327
I have a simple GenServer
within which I wish to create a loop that calls a function every two seconds:
defmodule MyModule do
use GenServer
def start_link(time) do
GenServer.start_link(__MODULE__,time)
end
#Start loop
def init(time) do
{:ok, myLoop(time)}
end
#Loop every two seconds
def myLoop(time) do
foo = bah(:someOtherProcess, {time})
IO.puts("The function value was: #{foo}")
:timer.sleep(2000)
myLoop(time + 2)
end
end
But when I call with:
{:ok, myServer} =MyModule.start_link(time)
IO.puts("Now I can carry on...")
I never see a return from the above call. This is kind of obvious I guess. So my question is, how can I create the loop I'd like without blocking the process from downstream execution tasks?
Thanks.
Upvotes: 1
Views: 1556
Reputation: 3809
You can now use handle_continue/2
to start your loop in the running process.
defmodule MyModule do
use GenServer
def start_link(time) do
GenServer.start_link(__MODULE__, time)
end
# Delegate starting loop to handle_continue/2
def init(time) do
{:ok, time, {:continue, :start_loop}}
end
# `time` is normally called `state` but we're making it more readable here
# I would normally encapsulate it in something like a struct, map or tuple
def handle_continue(:start_loop, time) do
# Start loop (after this function completes, when this process grabs its
# next message from its mailbox
# `self()` here returns the `pid` of the process you started in `init/1`
Process.send(self(), :loop)
{:noreply, time}
end
# `handle_info/2` will process any message that you receive that wasn't sent
# from a call to `GenServer.call/2` or `GenServer.cast/2`
# When we're handling these messages in our `handle_` functions, `self()` is always
# the `pid` of the process we sent the message to.
def handle_info(:loop, time) do
foo = bah(:some_other_process, {time})
IO.puts("The function value was: #{foo}")
# Run handle this function again in 2 seconds
Process.send_after(self(), :loop, 2000)
{:noreply, time + 2}
end
end
Upvotes: 0
Reputation: 2693
The best/cleanest way to accomplish what you are trying to do is with Process.send_after/3
. It delegates the timeout to the scheduler, not another process.
defmodule MyModule do
use GenServer
def start_link(time) do
GenServer.start_link(__MODULE__,time)
end
def init(time) do
schedule_do_something(time)
{:ok, %{time: time}}
end
def handle_info(:do_something, state) do
%{time: time} = state
# do something interesting here
schedule_do_something(time)
{:noreply, state}
end
defp schedule_do_something(time_in_seconds) do
Process.send_after(self, :do_something, (time_in_seconds * 1000))
end
end
A GenServer acts like an event loop anyway, so why reimplement this yourself?
Upvotes: 8
Reputation: 9559
It might be best to run another process that handles the timer loop and have it send a message to your genserver when it should perform the action. This way the GenEvent process is mostly idle to handle other messages, but it will be notified when it should take it's periodic action.
You could do this by running your MyModule.myloop
function in it's own process with spawn_link
. An example of this is below:
defmodule MyModule do
use GenServer
def start_link(time) do
GenServer.start_link(__MODULE__,time)
end
#Start loop
def init(time) do
spawn_link(&(MyModule.myloop(0, self))
{:ok, nil}
end
def handle_info({:timer, time}, state) do
foo = bah(:someOtherProcess, {time})
IO.puts("The function value was: #{foo}")
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
#Loop every two seconds
defp myLoop(time, parent) do
send(parent, {:timer, time})
:timer.sleep(2000)
myLoop(time + 2)
end
end
If you're not so bothered about passing the time in as part of the message (you could calculate it based on the GenServer state or similar) you could use the erlang function :timer.send_interval
which will send a message every period similar to the myLoop
function above. Documentation is here: http://www.erlang.org/doc/man/timer.html#send_interval-2
Upvotes: 0
Reputation: 84150
Since you are calling your loop inside your init function, your loop blocks infinitely and the init/1 callback never returns.
A common technique for performing an action on init is to send the GenServer a message and use handle_info/2 to perform an action. You should remember to include a catch all for handle_info
when doing this.
defmodule MyModule do
use GenServer
def start_link(time) do
{:ok, pid} = GenServer.start_link(__MODULE__,time)
send(pid, {:start_loop, time})
{:ok, pid}
end
#Start loop
def init(time) do
{:ok, nil}
end
def handle_info({:start_loop, time}, state) do
myLoop(time)
{:noreply, state}
end
#catch all
def handle_info(_, state) do
{:noreply, state}
end
#Loop every two seconds
def myLoop(time) do
IO.puts("The function value was: #{time}")
:timer.sleep(2000)
myLoop(time + 2)
end
end
Upvotes: 2