1
votes

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.

3

3 Answers

8
votes

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?

2
votes

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 
0
votes

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